# Keyboard selection and configuration spoke class # # Copyright (C) 2011-2012 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the # GNU General Public License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # # Red Hat Author(s): Chris Lumens # Vratislav Podzimek # from gi.repository import Gkbd, Gdk, Gtk from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.gui.categories.localization import LocalizationCategory from pyanaconda.ui.gui.utils import enlightbox, gtk_call_once, timed_action from pyanaconda import keyboard from pyanaconda import flags from pyanaconda.i18n import _, N_ from pyanaconda.constants import DEFAULT_KEYBOARD from pyanaconda.iutil import strip_accents, have_word_match import locale as locale_mod import logging log = logging.getLogger("anaconda") __all__ = ["KeyboardSpoke"] # %s will be replaced by key combination like Alt+Shift LAYOUT_SWITCHING_INFO = N_("%s to switch layouts.") def _show_layout(column, renderer, model, itr, wrapper): value = wrapper.get_layout_variant_description(model[itr][0]) renderer.set_property("text", value) def _show_description(column, renderer, model, itr, wrapper): value = wrapper.get_switch_opt_description(model[itr][0]) if model[itr][1]: value = "%s" % value renderer.set_property("markup", value) class AddLayoutDialog(GUIObject): builderObjects = ["addLayoutDialog", "newLayoutStore", "newLayoutStoreFilter", "newLayoutStoreSort"] mainWidgetName = "addLayoutDialog" uiFile = "spokes/keyboard.glade" def __init__(self, *args): GUIObject.__init__(self, *args) self._xkl_wrapper = keyboard.XklWrapper.get_instance() self._chosen_layouts = [] def matches_entry(self, model, itr, user_data=None): entry_text = self._entry.get_text() if not entry_text: # everything matches empty string return True value = model[itr][0] eng_value = self._xkl_wrapper.get_layout_variant_description(value, xlated=False) xlated_value = self._xkl_wrapper.get_layout_variant_description(value) translit_value = strip_accents(xlated_value).lower() translit_text = strip_accents(unicode(entry_text, "utf-8")).lower() return have_word_match(entry_text, eng_value) or have_word_match(entry_text, xlated_value) \ or have_word_match(translit_text, translit_value) def compare_layouts(self, model, itr1, itr2, user_data=None): """ We want to sort layouts by their show strings not their names. This function is an instance of GtkTreeIterCompareFunc(). """ value1 = model[itr1][0] value2 = model[itr2][0] show_str1 = self._xkl_wrapper.get_layout_variant_description(value1) show_str2 = self._xkl_wrapper.get_layout_variant_description(value2) return locale_mod.strcoll(show_str1, show_str2) def refresh(self): self._entry.grab_focus() def initialize(self): # We want to store layouts' names but show layouts as # 'language (description)'. self._entry = self.builder.get_object("addLayoutEntry") layoutColumn = self.builder.get_object("newLayoutColumn") layoutRenderer = self.builder.get_object("newLayoutRenderer") layoutColumn.set_cell_data_func(layoutRenderer, _show_layout, self._xkl_wrapper) self._treeModelFilter = self.builder.get_object("newLayoutStoreFilter") self._treeModelFilter.set_visible_func(self.matches_entry, None) self._treeModelSort = self.builder.get_object("newLayoutStoreSort") self._treeModelSort.set_default_sort_func(self.compare_layouts, None) self._store = self.builder.get_object("newLayoutStore") for layout in self._xkl_wrapper.get_available_layouts(): self._addLayout(self._store, layout) self._confirmAddButton = self.builder.get_object("confirmAddButton") self._newLayoutSelection = self.builder.get_object("newLayoutSelection") selected = self._newLayoutSelection.count_selected_rows() self._confirmAddButton.set_sensitive(selected) def run(self): self.window.show() rc = self.window.run() self.window.hide() return rc @property def chosen_layouts(self): return self._chosen_layouts def on_confirm_add_clicked(self, *args): treeview = self.builder.get_object("newLayoutView") selection = treeview.get_selection() (store, pathlist) = selection.get_selected_rows() self._chosen_layouts = [] for path in pathlist: itr = store.get_iter(path) self._chosen_layouts.append(store[itr][0]) def on_add_layout_selection_changed(self, selection): selected = selection.count_selected_rows() self._confirmAddButton.set_sensitive(selected) @timed_action() def on_entry_changed(self, *args): self._treeModelFilter.refilter() def on_entry_icon_clicked(self, *args): self._entry.set_text("") def on_layout_view_button_press(self, widget, event, *args): if event.type == Gdk.EventType._2BUTTON_PRESS and \ self._confirmAddButton.get_sensitive(): # double-click should close the dialog if something is selected # (i.e. the Add button is sensitive) # @see on_add_layout_selection_changed self._confirmAddButton.emit("clicked") # let the other actions happen as well return False def _addLayout(self, store, name): store.append([name]) class ConfigureSwitchingDialog(GUIObject): """Class representing a dialog for layout switching configuration""" builderObjects = ["switchingDialog", "switchingOptsStore", "switchingOptsSort",] mainWidgetName = "switchingDialog" uiFile = "spokes/keyboard.glade" def __init__(self, *args): GUIObject.__init__(self, *args) self._xkl_wrapper = keyboard.XklWrapper.get_instance() self._switchingOptsStore = self.builder.get_object("switchingOptsStore") def initialize(self): # we want to display "Alt + Shift" rather than "grp:alt_shift_toggle" descColumn = self.builder.get_object("descColumn") descRenderer = self.builder.get_object("descRenderer") descColumn.set_cell_data_func(descRenderer, _show_description, self._xkl_wrapper) self._switchingOptsSort = self.builder.get_object("switchingOptsSort") self._switchingOptsSort.set_default_sort_func(self._compare_options, None) for opt in self._xkl_wrapper.get_switching_options(): self._add_option(opt) def refresh(self): itr = self._switchingOptsStore.get_iter_first() while itr: option = self._switchingOptsStore[itr][0] if option in self.data.keyboard.switch_options: self._switchingOptsStore.set_value(itr, 1, True) else: self._switchingOptsStore.set_value(itr, 1, False) itr = self._switchingOptsStore.iter_next(itr) def run(self): rc = self.window.run() self.window.hide() return rc def _add_option(self, option): """Add option to the list as unchecked""" self._switchingOptsStore.append([option, False]) def _compare_options(self, model, itr1, itr2, user_data=None): """ We want to sort options by their show strings not their names. This function is an instance of GtkTreeIterCompareFunc(). """ value1 = model[itr1][0] value2 = model[itr2][0] show_str1 = self._xkl_wrapper.get_switch_opt_description(value1) show_str2 = self._xkl_wrapper.get_switch_opt_description(value2) if show_str1 < show_str2: return -1 elif show_str1 == show_str2: return 0 else: return 1 @property def checked_options(self): """Property returning all checked options from the list""" ret = [row[0] for row in self._switchingOptsSort if row[1] and row[0]] return ret def on_use_option_toggled(self, renderer, path, *args): itr = self._switchingOptsSort.get_iter(path) # Get itr for the *store*. itr = self._switchingOptsSort.convert_iter_to_child_iter(itr) old_value = self._switchingOptsStore[itr][1] self._switchingOptsStore.set_value(itr, 1, not old_value) class KeyboardSpoke(NormalSpoke): builderObjects = ["addedLayoutStore", "keyboardWindow", "layoutTestBuffer"] mainWidgetName = "keyboardWindow" uiFile = "spokes/keyboard.glade" category = LocalizationCategory icon = "input-keyboard-symbolic" title = N_("_KEYBOARD") def __init__(self, *args): NormalSpoke.__init__(self, *args) self._remove_last_attempt = False self._confirmed = False self._xkl_wrapper = keyboard.XklWrapper.get_instance() self._add_dialog = None self._upButton = self.builder.get_object("upButton") self._downButton = self.builder.get_object("downButton") self._removeButton = self.builder.get_object("removeLayoutButton") self._previewButton = self.builder.get_object("previewButton") def apply(self): # the user has confirmed (seen) the configuration self._confirmed = True # Clear and repopulate self.data with actual values self.data.keyboard.x_layouts = list() self.data.keyboard.seen = True for row in self._store: self.data.keyboard.x_layouts.append(row[0]) @property def completed(self): if flags.flags.automatedInstall and not self.data.keyboard.seen: return False elif not self._confirmed and self._xkl_wrapper.get_current_layout() != self.data.keyboard.x_layouts[0]: # the currently activated layout is a different one from the # installed system's default return False else: return True @property def status(self): # We don't need to check that self._store is empty, because that isn't allowed. descriptions = (self._xkl_wrapper.get_layout_variant_description(row[0]) for row in self._store) return ", ".join(descriptions) def initialize(self): NormalSpoke.initialize(self) if flags.can_touch_runtime_system("hide runtime keyboard configuration " "warning", touch_live=True): self.builder.get_object("warningBox").hide() # We want to store layouts' names but show layouts as # 'language (description)'. layoutColumn = self.builder.get_object("layoutColumn") layoutRenderer = self.builder.get_object("layoutRenderer") layoutColumn.set_cell_data_func(layoutRenderer, _show_layout, self._xkl_wrapper) self._store = self.builder.get_object("addedLayoutStore") self._add_data_layouts() self._switching_dialog = ConfigureSwitchingDialog(self.data) self._switching_dialog.initialize() self._layoutSwitchLabel = self.builder.get_object("layoutSwitchLabel") if not flags.can_touch_runtime_system("test X layouts", touch_live=True): # Disable area for testing layouts as we cannot make # it work without modifying runtime system widgets = [self.builder.get_object("testingLabel"), self.builder.get_object("testingWindow"), self.builder.get_object("layoutSwitchLabel")] # Use testingLabel's text to explain why this part is not # sensitive. widgets[0].set_text(_("Testing layouts configuration not " "available.")) for widget in widgets: widget.set_sensitive(False) def refresh(self): NormalSpoke.refresh(self) # Clear out the layout testing box every time the spoke is loaded. It # doesn't make sense to leave temporary data laying around. buf = self.builder.get_object("layoutTestBuffer") buf.set_text("") # Clear and repopulate addedLayoutStore with values from self.data self._store.clear() self._add_data_layouts() # Start with no buttons enabled, since nothing is selected. self._upButton.set_sensitive(False) self._downButton.set_sensitive(False) self._removeButton.set_sensitive(False) self._previewButton.set_sensitive(False) self._refresh_switching_info() def _addLayout(self, store, name): # first try to add the layout if flags.can_touch_runtime_system("add runtime X layout", touch_live=True): self._xkl_wrapper.add_layout(name) # valid layout, append it to the store store.append([name]) def _removeLayout(self, store, itr): """ Remove the layout specified by store iterator from the store and X runtime configuration. """ if flags.can_touch_runtime_system("remove runtime X layout", touch_live=True): self._xkl_wrapper.remove_layout(store[itr][0]) store.remove(itr) def _refresh_switching_info(self): if self.data.keyboard.switch_options: first_option = self.data.keyboard.switch_options[0] desc = self._xkl_wrapper.get_switch_opt_description(first_option) self._layoutSwitchLabel.set_text(_(LAYOUT_SWITCHING_INFO) % desc) else: self._layoutSwitchLabel.set_text(_("Layout switching not " "configured.")) # Signal handlers. def on_add_clicked(self, button): if not self._add_dialog: self._add_dialog = AddLayoutDialog(self.data) self._add_dialog.initialize() self._add_dialog.refresh() with enlightbox(self.window, self._add_dialog.window): response = self._add_dialog.run() if response == 1: duplicates = set() for row in self._store: item = row[0] if item in self._add_dialog.chosen_layouts: duplicates.add(item) for layout in self._add_dialog.chosen_layouts: if layout not in duplicates: self._addLayout(self._store, layout) if self._remove_last_attempt: itr = self._store.get_iter_first() if not self._store[itr][0] in self._add_dialog.chosen_layouts: self._removeLayout(self._store, itr) self._remove_last_attempt = False def on_remove_clicked(self, button): selection = self.builder.get_object("layoutSelection") if not selection.count_selected_rows(): return (store, itr) = selection.get_selected() itr2 = store.get_iter_first() #if the first item is selected, try to select the next one if store[itr][0] == store[itr2][0]: itr2 = store.iter_next(itr2) if itr2: #next one existing selection.select_iter(itr2) self._removeLayout(store, itr) return #nothing left, run AddLayout dialog to replace the current layout #add it to GLib.idle to make sure the underlaying gui is correctly #redrawn self._remove_last_attempt = True add_button = self.builder.get_object("addLayoutButton") gtk_call_once(self.on_add_clicked, add_button) return #the selected item is not the first, select the previous one #XXX: there is no model.iter_previous() so we have to find it this way itr3 = store.iter_next(itr2) #look-ahead iterator while itr3 and (store[itr3][0] != store[itr][0]): itr2 = store.iter_next(itr2) itr3 = store.iter_next(itr3) self._removeLayout(store, itr) selection.select_iter(itr2) def on_up_clicked(self, button): selection = self.builder.get_object("layoutSelection") if not selection.count_selected_rows(): return (store, cur) = selection.get_selected() prev = store.iter_previous(cur) if not prev: return store.swap(cur, prev) if flags.can_touch_runtime_system("reorder runtime X layouts", touch_live=True): self._flush_layouts_to_X() if not store.iter_previous(cur): #layout is first in the list (set as default), activate it self._xkl_wrapper.activate_default_layout() selection.emit("changed") def on_down_clicked(self, button): selection = self.builder.get_object("layoutSelection") if not selection.count_selected_rows(): return (store, cur) = selection.get_selected() #if default layout (first in the list) changes we need to activate it activate_default = not store.iter_previous(cur) nxt = store.iter_next(cur) if not nxt: return store.swap(cur, nxt) if flags.can_touch_runtime_system("reorder runtime X layouts", touch_live=True): self._flush_layouts_to_X() if activate_default: self._xkl_wrapper.activate_default_layout() selection.emit("changed") def on_preview_clicked(self, button): selection = self.builder.get_object("layoutSelection") (store, cur) = selection.get_selected() layout_row = store[cur] if not layout_row: return layout, variant = keyboard.parse_layout_variant(layout_row[0]) if variant: lay_var_spec = "%s\t%s" % (layout, variant) else: lay_var_spec = layout dialog = Gkbd.KeyboardDrawing.dialog_new() Gkbd.KeyboardDrawing.dialog_set_layout(dialog, self._xkl_wrapper.configreg, lay_var_spec) dialog.set_size_request(750, 350) dialog.set_position(Gtk.WindowPosition.CENTER_ALWAYS) with enlightbox(self.window, dialog): dialog.show_all() dialog.run() def on_selection_changed(self, selection, *args): # We don't have to worry about multiple rows being selected in this # function, because that's disabled by the widget. if not selection.count_selected_rows(): self._upButton.set_sensitive(False) self._downButton.set_sensitive(False) self._removeButton.set_sensitive(False) self._previewButton.set_sensitive(False) return (store, selected) = selection.get_selected_rows() # If something's selected, always enable the remove and preview buttons. self._removeButton.set_sensitive(True) self._previewButton.set_sensitive(True) # Disable the Up button if the top row's selected, and disable the # Down button if the bottom row's selected. if selected[0].get_indices() == [0]: self._upButton.set_sensitive(False) self._downButton.set_sensitive(True) elif selected[0].get_indices() == [len(store)-1]: self._upButton.set_sensitive(True) self._downButton.set_sensitive(False) else: self._upButton.set_sensitive(True) self._downButton.set_sensitive(True) def on_options_clicked(self, *args): self._switching_dialog.refresh() with enlightbox(self.window, self._switching_dialog.window): response = self._switching_dialog.run() if response != 1: # Cancel clicked, dialog destroyed return # OK clicked, set and save switching options. new_options = self._switching_dialog.checked_options self._xkl_wrapper.set_switching_options(new_options) self.data.keyboard.switch_options = new_options # Refresh switching info label. self._refresh_switching_info() def _add_data_layouts(self): if not self.data.keyboard.x_layouts: # nothing specified, just add the default self._addLayout(self._store, DEFAULT_KEYBOARD) return valid_layouts = [] for layout in self.data.keyboard.x_layouts: try: self._addLayout(self._store, layout) valid_layouts += layout except keyboard.XklWrapperError: log.error("Failed to add layout '%s'", layout) if not valid_layouts: log.error("No valid layout given, falling back to default %s", DEFAULT_KEYBOARD) self._addLayout(self._store, DEFAULT_KEYBOARD) self.data.keyboard.x_layouts = [DEFAULT_KEYBOARD] def _flush_layouts_to_X(self): layouts_list = list() for row in self._store: layouts_list.append(row[0]) self._xkl_wrapper.replace_layouts(layouts_list)