class SettingsView(View): """Generic GSettingsView in a modern Gnome like appearance.""" def __init__(self, app): View.__init__( self, app, sub_title='Configure how duplicates are searched' ) self._grid = Gtk.Grid() self._grid.set_margin_start(30) self._grid.set_margin_end(40) self._grid.set_margin_top(5) self._grid.set_margin_bottom(15) self._grid.set_hexpand(True) self._grid.set_vexpand(True) self._grid.set_halign(Gtk.Align.FILL) self._grid.set_valign(Gtk.Align.FILL) self.add(self._grid) self.save_settings = False self.sections = {} self.metadata = {} self.appy_btn = SuggestedButton() self.deny_btn = DestructiveButton('Reset to defaults') self.appy_btn.connect('clicked', self.on_apply_settings) self.deny_btn.connect('clicked', self.on_reset_to_defaults) self.search_entry.connect( 'search-changed', self.on_search_changed ) # Initialize from current settings: self.build() def append_section(self, heading): """Append a new named section for multiple entries with `heading`""" box = Gtk.ListBox() box.set_selection_mode(Gtk.SelectionMode.NONE) box.set_size_request(350, -1) box.set_hexpand(True) frame = Gtk.Frame() frame.set_halign(Gtk.Align.FILL) frame.add(box) label = Gtk.Label() label.set_margin_top(30) label.set_markup( '<b>{}:</b>'.format(GLib.markup_escape_text(heading, -1)) ) label.set_halign(Gtk.Align.START) label.set_margin_bottom(2) self.sections[heading.lower()] = box self.metadata[heading.lower()] = { 'label': label, 'frame': frame } self._grid.attach(label, 0, len(self._grid), 1, 1) self._grid.attach(frame, 0, len(self._grid), 1, 1) def append_entry(self, section, val_widget, key_name, summary, desc=None): """Append an entry to a named section. section: A previously inserted section name. val_widget: The widget to show the key in. summary: A short summary to show. desc: A longer description. """ desc_label = Gtk.Label(desc or '') summ_label = Gtk.Label(summary or '') desc_label.get_style_context().add_class( Gtk.STYLE_CLASS_DIM_LABEL ) for label in desc_label, summ_label: label.set_use_markup(True) label.set_halign(Gtk.Align.FILL) label.set_hexpand(True) label.set_halign(Gtk.Align.START) val_widget.set_halign(Gtk.Align.END) sub_grid = Gtk.Grid() sub_grid.attach(summ_label, 0, 0, 1, 1) sub_grid.attach(desc_label, 0, 1, 1, 1) sub_grid.attach(val_widget, 1, 0, 1, 2) sub_grid.set_border_width(3) listbox = self.sections[section.lower()] if len(listbox): sep_row = Gtk.ListBoxRow() sep_row.set_activatable(False) sep_row.add(Gtk.Separator()) listbox.insert(sep_row, -1) row = Gtk.ListBoxRow() row.add(sub_grid) row.set_can_focus(False) listbox.insert(row, -1) self.metadata[section.lower()][key_name] = { 'summary': summary.lower() or '', 'description': desc.lower() or '', 'widget': row } def reset_to_defaults(self): """Reset whole view and keys to their defaults""" for key_name in self.app.settings.list_keys(): self.app.settings.reset(key_name) def build(self): """Built all entries and sections""" gst = self.app.settings entry_rows = [] for key_name in gst.list_keys(): key = gst.get_property('settings-schema').get_key(key_name) variant_key = gst.get_value(key_name) # Try to find a way to render this option: constructor = VARIANT_TO_WIDGET.get(variant_key.get_type_string()) if constructor is None: continue # Get the key summary and description: summary = '{}'.format(key.get_summary()) # This is an extension of this code: if summary.startswith('[hidden]'): continue order, order_grep = 0, re.match(r'\[(\d+)]\s(.*)', summary) if order_grep is not None: order, summary = int(order_grep.group(1)), order_grep.group(2) description = key.get_description() if description: description = '<small>{desc}</small>'.format( desc=key.get_description() ) # Get a fitting, readily prepared configure widget val_widget = constructor(gst, key_name, summary, description) # Try to find the section name by the keyname. # This is an extension made by this code and not part of GSettings. section = '' if '-' in key_name: section, _ = key_name.split('-', maxsplit=1) entry_rows.append( (order, section, val_widget, key_name, summary, description) ) for section in sorted(set([entry[1] for entry in entry_rows])): self.append_section(section.capitalize()) for entry in sorted(entry_rows, key=itemgetter(0, 2)): self.append_entry(*entry[1:]) #################### # SIGNAL CALLBACKS # #################### def on_search_changed(self, _): """Called once the user enteres a new search query.""" query = self.search_entry.get_text().lower() def _set_vis(widget, state, lower): """Set opacity and sensitivity in one.""" widget.set_sensitive(state) widget.set_opacity(1.0 if state else lower) for _, metadata in self.metadata.items(): section_visible = 0 for key_name, info in metadata.items(): if key_name in ['label', 'frame']: continue row = info['widget'] if query in info['summary'] or query in info['description']: section_visible += 1 _set_vis(row, True, 0.5) else: _set_vis(row, False, 0.5) section_frame = metadata['frame'] section_label = metadata['label'] _set_vis(section_frame, section_visible > 0, 0.2) _set_vis(section_label, section_visible > 0, 0.2) def on_view_enter(self): """Called once the view is visible. Delay save of settings.""" self.save_settings = False self.app.settings.delay() # Give the buttons a specific context meaning: self.on_key_changed(self.app.settings, None) self.app.settings.connect('changed', self.on_key_changed) self.add_header_widget(self.appy_btn) self.add_header_widget(self.deny_btn, Gtk.Align.START) # It's usually more useful when switching back to the latest view, # not to the view to the right since user might just have wanted # to have a quick settings change from anywhere in the application. self.app_window.views.switch_to_previous_next() def on_view_leave(self): """Called once the view gets out of sight. Revert or apply.""" if self.save_settings: self.app.settings.apply() else: self.app.settings.revert() self.clear_header_widgets() def on_apply_settings(self, *_): """Callback for the apply button.""" self.save_settings = True self.app_window.views.switch_to_previous() def on_reset_to_defaults(self, *_): """Callback for the reset button.""" self.app.settings.revert() GLib.timeout_add( 100, lambda: self.reset_to_defaults() or self.app.settings.delay() ) self.save_settings = False self.app_window.views.switch_to_previous() def on_key_changed(self, settings, _): """Called when a key in GSettings changes.""" is_sensitive = settings.get_has_unapplied() self.appy_btn.set_sensitive(is_sensitive) self.deny_btn.set_sensitive(is_sensitive) def on_default_action(self): """Called on Ctrl-Enter""" if self.appy_btn.is_sensitive(): self.on_apply_settings() else: self.app_window.views.switch_to_previous()
class SettingsView(View): """Generic GSettingsView in a modern Gnome like appearance.""" def __init__(self, app): View.__init__(self, app, sub_title='Tweak how files are synchronized') self._grid = Gtk.Grid() self._grid.set_margin_start(30) self._grid.set_margin_end(40) self._grid.set_margin_top(5) self._grid.set_margin_bottom(15) self._grid.set_hexpand(True) self._grid.set_vexpand(True) self._grid.set_halign(Gtk.Align.FILL) self._grid.set_valign(Gtk.Align.FILL) self.add(self._grid) self.save_settings = False self.sections = {} self.metadata = {} self.appy_btn = SuggestedButton() self.deny_btn = DestructiveButton('Reset to defaults') self.appy_btn.connect('clicked', self.on_apply_settings) self.deny_btn.connect('clicked', self.on_reset_to_defaults) self.search_entry.connect('search-changed', self.on_search_changed) # Initialize from current settings: self.build() def append_section(self, heading): """Append a new named section for multiple entries with `heading`""" box = Gtk.ListBox() box.set_selection_mode(Gtk.SelectionMode.NONE) box.set_size_request(350, -1) box.set_hexpand(True) frame = Gtk.Frame() frame.set_halign(Gtk.Align.FILL) frame.add(box) label = Gtk.Label() label.set_margin_top(30) label.set_markup('<b>{}:</b>'.format( GLib.markup_escape_text(heading, -1))) label.set_halign(Gtk.Align.START) label.set_margin_bottom(2) self.sections[heading.lower()] = box self.metadata[heading.lower()] = {'label': label, 'frame': frame} self._grid.attach(label, 0, len(self._grid), 1, 1) self._grid.attach(frame, 0, len(self._grid), 1, 1) def append_entry(self, section, val_widget, key_name, summary, desc=None): """Append an entry to a named section. section: A previously inserted section name. val_widget: The widget to show the key in. summary: A short summary to show. desc: A longer description. """ desc_label = Gtk.Label(desc or '') summ_label = Gtk.Label(summary or '') desc_label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL) for label in desc_label, summ_label: label.set_use_markup(True) label.set_halign(Gtk.Align.FILL) label.set_hexpand(True) label.set_halign(Gtk.Align.START) val_widget.set_halign(Gtk.Align.END) sub_grid = Gtk.Grid() sub_grid.attach(summ_label, 0, 0, 1, 1) sub_grid.attach(desc_label, 0, 1, 1, 1) sub_grid.attach(val_widget, 1, 0, 1, 2) sub_grid.set_border_width(3) listbox = self.sections[section.lower()] if len(listbox): sep_row = Gtk.ListBoxRow() # Needs gtk >= 3.14 if hasattr(sep_row, 'set_activatable'): sep_row.set_activatable(False) sep_row.add(Gtk.Separator()) listbox.insert(sep_row, -1) row = Gtk.ListBoxRow() row.add(sub_grid) row.set_can_focus(False) listbox.insert(row, -1) self.metadata[section.lower()][key_name] = { 'summary': summary.lower() or '', 'description': desc.lower() or '', 'widget': row } def reset_to_defaults(self): """Reset whole view and keys to their defaults""" for key_name in self.app.settings.list_keys(): self.app.settings.reset(key_name) def build(self): """Built all entries and sections""" gst = self.app.settings entry_rows = [] for key_name in gst.list_keys(): key = gst.get_property('settings-schema').get_key(key_name) variant_key = gst.get_value(key_name) # Try to find a way to render this option: constructor = VARIANT_TO_WIDGET.get(variant_key.get_type_string()) if constructor is None: continue # Get the key summary and description: summary = '{}'.format(key.get_summary()) # This is an extension of this code: if summary.startswith('[hidden]'): continue order, order_grep = 0, re.match(r'\[(\d+)]\s(.*)', summary) if order_grep is not None: order, summary = int(order_grep.group(1)), order_grep.group(2) description = key.get_description() if description: description = '<small>{desc}</small>'.format( desc=key.get_description()) # Get a fitting, readily prepared configure widget val_widget = constructor(gst, key_name, summary, description) # Try to find the section name by the keyname. # This is an extension made by this code and not part of GSettings. section = '' if '-' in key_name: section, _ = key_name.split('-', maxsplit=1) entry_rows.append( (order, section, val_widget, key_name, summary, description)) for section in sorted(set([entry[1] for entry in entry_rows])): self.append_section(section.capitalize()) for entry in sorted(entry_rows, key=itemgetter(0, 2)): self.append_entry(*entry[1:]) #################### # SIGNAL CALLBACKS # #################### def on_search_changed(self, _): """Called once the user enteres a new search query.""" query = self.search_entry.get_text().lower() def _set_vis(widget, state, lower): """Set opacity and sensitivity in one.""" widget.set_sensitive(state) widget.set_opacity(1.0 if state else lower) for _, metadata in self.metadata.items(): section_visible = 0 for key_name, info in metadata.items(): if key_name in ['label', 'frame']: continue row = info['widget'] if query in info['summary'] or query in info['description']: section_visible += 1 _set_vis(row, True, 0.5) else: _set_vis(row, False, 0.5) section_frame = metadata['frame'] section_label = metadata['label'] _set_vis(section_frame, section_visible > 0, 0.2) _set_vis(section_label, section_visible > 0, 0.2) def on_view_enter(self): """Called once the view is visible. Delay save of settings.""" self.save_settings = False self.app.settings.delay() # Give the buttons a specific context meaning: self.on_key_changed(self.app.settings, None) self.app.settings.connect('changed', self.on_key_changed) self.add_header_widget(self.appy_btn) self.add_header_widget(self.deny_btn, Gtk.Align.START) # It's usually more useful when switching back to the latest view, # not to the view to the right since user might just have wanted # to have a quick settings change from anywhere in the application. self.app_window.views.switch_to_previous_next() def on_view_leave(self): """Called once the view gets out of sight. Revert or apply.""" if self.save_settings: self.app.settings.apply() else: self.app.settings.revert() self.clear_header_widgets() def on_apply_settings(self, *_): """Callback for the apply button.""" self.save_settings = True self.app_window.views.switch_to_previous() def on_reset_to_defaults(self, *_): """Callback for the reset button.""" self.app.settings.revert() GLib.timeout_add( 100, lambda: self.reset_to_defaults() or self.app.settings.delay()) self.save_settings = False self.app_window.views.switch_to_previous() def on_key_changed(self, settings, _): """Called when a key in GSettings changes.""" is_sensitive = settings.get_has_unapplied() self.appy_btn.set_sensitive(is_sensitive) self.deny_btn.set_sensitive(is_sensitive) def on_default_action(self): """Called on Ctrl-Enter""" if self.appy_btn.is_sensitive(): self.on_apply_settings() else: self.app_window.views.switch_to_previous()
class ScriptSaverDialog(Gtk.FileChooserWidget): """GtkFileChooserWidget tailored for saving a `Script` instance.""" __gsignals__ = { 'saved': (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, editor_view): Gtk.FileChooserWidget.__init__(self) self.editor_view = editor_view self.set_select_multiple(False) self.set_create_folders(False) self.set_action(Gtk.FileChooserAction.SAVE) self.set_do_overwrite_confirmation(True) self.file_type = MultipleChoiceButton(['sh', 'json', 'csv'], 'sh', 'sh') self.file_type.set_halign(Gtk.Align.START) self.file_type.set_hexpand(True) self.file_type.connect('row-selected', self.on_file_type_changed) self.file_type.props.margin_end = 10 self.confirm = SuggestedButton('Save') self.confirm.connect('clicked', self.on_save_clicked) self.confirm.set_halign(Gtk.Align.END) self.confirm.set_hexpand(False) self.confirm.set_sensitive(False) self.confirm.props.margin_end = 10 self.cancel_button = IconButton('window-close-symbolic', 'Cancel') self.cancel_button.connect('clicked', self.on_cancel_clicked) self.cancel_button.set_halign(Gtk.Align.END) self.cancel_button.set_hexpand(False) self.connect('selection-changed', self.on_selection_changed) file_type_label = Gtk.Label('<b>Filetype</b>') file_type_label.set_use_markup(True) file_type_label.props.margin_end = 5 file_type_label.get_style_context().add_class( Gtk.STYLE_CLASS_DIM_LABEL) self.extra_box = Gtk.Grid() self.extra_box.attach(self.file_type, 0, 0, 1, 1) self.extra_box.attach(self.confirm, 1, 0, 1, 1) self.extra_box.attach_next_to(file_type_label, self.file_type, Gtk.PositionType.LEFT, 1, 1) self.extra_box.set_hexpand(True) self.extra_box.set_halign(Gtk.Align.FILL) def show_controls(self): """Show cancel, save and file type chooser buttons.""" self.editor_view.add_header_widget(self.extra_box) self.editor_view.add_header_widget(self.cancel_button, align=Gtk.Align.START) self.update_file_suggestion() def update_file_suggestion(self): """Suggest a name for the script to save.""" file_type = self.file_type.get_selected_choice() or 'sh' self.set_current_name(time.strftime('rmlint-%FT%T%z.' + file_type)) def on_file_type_changed(self, _): """Called once the user chose a different format""" current_path = self.get_filename() if not current_path: self.update_file_suggestion() else: try: path, _ = current_path.rsplit('.', 1) self.set_current_name( path + '.' + self.file_type.get_selected_choice() or '') except ValueError: # No extension. Leave it. pass def _exit_from_save(self): """Preparation to go back to script view.""" self.emit('saved') self.editor_view.clear_header_widgets() def on_cancel_clicked(self, _): """Signal handler for the cancel button.""" self._exit_from_save() def on_save_clicked(self, _): """Called once the user clicked the `Save` button""" file_type = self.file_type.get_selected_choice() abs_path = self.get_filename() runner = self.editor_view.app_window.views['runner'].runner LOGGER.info('Saving script as `%s` to: %s', file_type, abs_path) runner.save(abs_path, file_type) self._exit_from_save() def on_selection_changed(self, _): """Called once a file or directory was clicked""" filename = self.get_filename() self.confirm.set_sensitive(bool(filename)) # Make sure the user-typed extension gets set in teh type chooser also. name = self.get_current_name() *_, extension = name.rsplit('.', 1) self.file_type.set_selected_choice(extension)
class ScriptSaverDialog(Gtk.FileChooserWidget): """GtkFileChooserWidget tailored for saving a `Script` instance.""" __gsignals__ = { 'saved': (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, editor_view): Gtk.FileChooserWidget.__init__(self) self.editor_view = editor_view self.set_select_multiple(False) self.set_create_folders(False) self.set_action(Gtk.FileChooserAction.SAVE) self.set_do_overwrite_confirmation(True) self.file_type = MultipleChoiceButton( ['sh', 'json', 'csv'], 'sh', 'sh' ) self.file_type.set_halign(Gtk.Align.START) self.file_type.set_hexpand(True) self.file_type.connect('row-selected', self.on_file_type_changed) self.file_type.props.margin_end = 10 self.confirm = SuggestedButton('Save') self.confirm.connect('clicked', self.on_save_clicked) self.confirm.set_halign(Gtk.Align.END) self.confirm.set_hexpand(False) self.confirm.set_sensitive(False) self.confirm.props.margin_end = 10 self.cancel_button = IconButton('window-close-symbolic', 'Cancel') self.cancel_button.connect('clicked', self.on_cancel_clicked) self.cancel_button.set_halign(Gtk.Align.END) self.cancel_button.set_hexpand(False) self.connect('selection-changed', self.on_selection_changed) file_type_label = Gtk.Label('<b>Filetype</b>') file_type_label.set_use_markup(True) file_type_label.props.margin_end = 5 file_type_label.get_style_context().add_class( Gtk.STYLE_CLASS_DIM_LABEL ) self.extra_box = Gtk.Grid() self.extra_box.attach(self.file_type, 0, 0, 1, 1) self.extra_box.attach(self.confirm, 1, 0, 1, 1) self.extra_box.attach_next_to( file_type_label, self.file_type, Gtk.PositionType.LEFT, 1, 1 ) self.extra_box.set_hexpand(True) self.extra_box.set_halign(Gtk.Align.FILL) def show_controls(self): """Show cancel, save and file type chooser buttons.""" self.editor_view.add_header_widget(self.extra_box) self.editor_view.add_header_widget( self.cancel_button, align=Gtk.Align.START ) self.update_file_suggestion() def update_file_suggestion(self): """Suggest a name for the script to save.""" file_type = self.file_type.get_selected_choice() or 'sh' self.set_current_name(time.strftime('rmlint-%FT%T%z.' + file_type)) def on_file_type_changed(self, _): """Called once the user chose a different format""" current_path = self.get_filename() if not current_path: self.update_file_suggestion() else: try: path, _ = current_path.rsplit('.', 1) self.set_current_name( path + '.' + self.file_type.get_selected_choice() or '' ) except ValueError: # No extension. Leave it. pass def _exit_from_save(self): """Preparation to go back to script view.""" self.emit('saved') self.editor_view.clear_header_widgets() def on_cancel_clicked(self, _): """Signal handler for the cancel button.""" self._exit_from_save() def on_save_clicked(self, _): """Called once the user clicked the `Save` button""" file_type = self.file_type.get_selected_choice() abs_path = self.get_filename() runner = self.editor_view.app_window.views['runner'].runner LOGGER.info('Saving script as `%s` to: %s', file_type, abs_path) runner.save(abs_path, file_type) self._exit_from_save() def on_selection_changed(self, _): """Called once a file or directory was clicked""" filename = self.get_filename() self.confirm.set_sensitive(bool(filename)) # Make sure the user-typed extension gets set in teh type chooser also. name = self.get_current_name() *_, extension = name.rsplit('.', 1) self.file_type.set_selected_choice(extension)