diff --git a/action-layout-editor/nemo_action_layout_editor.py b/action-layout-editor/nemo_action_layout_editor.py index 8a329cda6..83e98603b 100644 --- a/action-layout-editor/nemo_action_layout_editor.py +++ b/action-layout-editor/nemo_action_layout_editor.py @@ -1,17 +1,11 @@ #!/usr/bin/python3 import gi gi.require_version('Gtk', '3.0') -gi.require_version('XApp', '1.0') -gi.require_version('Xmlb', '2.0') -from gi.repository import Gtk, Gdk, GLib, Gio, XApp, GdkPixbuf, Pango, Xmlb -import cairo -import json -from pathlib import Path -import uuid +gi.require_version('Nemo', '3.0') +from gi.repository import Gtk, Gdk, GLib, Gio, Nemo +import subprocess import gettext import locale -from collections import OrderedDict -import subprocess import os import leconfig @@ -21,1525 +15,35 @@ gettext.textdomain("nemo") _ = gettext.gettext -gresources = Gio.Resource.load(os.path.join(leconfig.PKG_DATADIR, "nemo-action-layout-editor-resources.gresource")) -gresources._register() - -JSON_FILE = Path(GLib.get_user_config_dir()).joinpath("nemo/actions-tree.json") -USER_ACTIONS_DIR = Path(GLib.get_user_data_dir()).joinpath("nemo/actions") - -NON_SPICE_UUID_SUFFIX = "@untracked" - -ROW_HASH, ROW_UUID, ROW_TYPE, ROW_OBJ = range(4) - -ROW_TYPE_ACTION = "action" -ROW_TYPE_SUBMENU = "submenu" -ROW_TYPE_SEPARATOR = "separator" - -def new_hash(): - return uuid.uuid4().hex - -class BuiltinShortcut(): - def __init__(self, label, accel_string): - self.key, self.mods = Gtk.accelerator_parse(accel_string) - - if self.key == 0 and self.mods == 0: - self.label = "invalid (%s)" % accel_string - - self.label = _(label) - -class Row(): - def __init__(self, row_meta=None, keyfile=None, path=None, enabled=True, scale_factor=1): - self.keyfile = keyfile - self.row_meta = row_meta - self.enabled = enabled - self.scale_factor = scale_factor - self.path = path # PosixPath - - def get_icon_string(self, original=False): - icon_string = None - - if self.row_meta and not original: - user_assigned_name = self.row_meta.get('user-icon', None) - if user_assigned_name is not None: - icon_string = user_assigned_name - - if icon_string is None: - if self.keyfile is not None: - try: - icon_string = self.keyfile.get_string('Nemo Action', 'Icon-Name') - except GLib.Error: - pass - - return icon_string - - def get_path(self): - return self.path - - def get_icon_type_and_data(self, original=False): - icon_string = self.get_icon_string(original) - - if icon_string is None: - return None - - if icon_string.startswith("<") and icon_string.endswith(">"): - real_string = icon_string[1:-1] - icon_string = str(self.path.parent / real_string) - - if GLib.path_is_absolute(icon_string): - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_string, 16, 16) - surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, self.scale_factor, None) - return ("surface", surface) - - return ("icon-name", icon_string) - - def get_label(self): - if self.row_meta is not None: - if self.row_meta.get("type") == ROW_TYPE_SEPARATOR: - return "──────────────────────────────" - - label = None - - if self.row_meta is not None: - user_assigned_label = self.row_meta.get('user-label', None) - if user_assigned_label is not None: - label = user_assigned_label - - if label is None: - if self.keyfile is not None: - try: - label = self.keyfile.get_locale_string('Nemo Action', 'Name', None).replace("_", "") - except GLib.Error as e: - print(e) - pass - - if label is None: - return _("Unknown") - - return label - - def get_accelerator_string(self): - if self.row_meta is not None: - try: - accel_string = self.row_meta['accelerator'] - if accel_string is not None: - return accel_string - except KeyError: - pass - - return None - - def set_custom_label(self, label): - if not self.row_meta: - self.row_meta = {} - - self.row_meta['user-label'] = label - - def set_custom_icon(self, icon): - if not self.row_meta: - self.row_meta = {} - - self.row_meta['user-icon'] = icon - - def set_accelerator_string(self, accel_string): - if not self.row_meta: - self.row_meta = {} - - self.row_meta['accelerator'] = accel_string - - def get_custom_label(self): - if self.row_meta: - return self.row_meta.get('user-label') - return None - - def get_custom_icon(self): - if self.row_meta: - return self.row_meta.get('user-icon') - return None - -class NemoActionsOrganizer(Gtk.Box): - def __init__(self, window, builder=None): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) - - if builder is None: - self.builder = Gtk.Builder.new_from_resource("/org/nemo/action-layout-editor/nemo-action-layout-editor.glade") - else: - self.builder = builder - - self.builtin_shortcuts = [] - self.load_nemo_shortcuts() - - self.main_window = window - self.layout_editor_box = self.builder.get_object("layout_editor_box") - self.add(self.layout_editor_box) - self.treeview_holder = self.builder.get_object("treeview_holder") - self.save_button = self.builder.get_object("save_button") - self.discard_changes_button = self.builder.get_object("discard_changes_button") - self.default_layout_button = self.builder.get_object("default_layout_button") - self.name_entry = self.builder.get_object("name_entry") - self.new_row_button = self.builder.get_object("new_row_button") - self.row_controls_box = self.builder.get_object("row_controls_box") - self.remove_submenu_button = self.builder.get_object("remove_submenu_button") - self.clear_icon_button = self.builder.get_object("clear_icon_button") - self.icon_selector_menu_button = self.builder.get_object("icon_selector_menu_button") - self.icon_selector_image = self.builder.get_object("icon_selector_image") - self.selected_item_widgets_group = XApp.VisibilityGroup.new(True, True, [ - self.icon_selector_menu_button, - self.name_entry - ]) - - self.up_button = self.builder.get_object("up_button") - self.up_button.connect("clicked", self.up_button_clicked) - self.down_button = self.builder.get_object("down_button") - self.down_button.connect("clicked", self.down_button_clicked) - - self.nemo_plugin_settings = Gio.Settings(schema_id="org.nemo.plugins") - # Disabled/Enabled may be toggled in nemo preferences directly, keep us in sync. - self.nemo_plugin_settings.connect("changed", self.on_disabled_settings_list_changed) - - # Icon MenuButton - menu = Gtk.Menu() - - self.blank_icon_menu_item = Gtk.ImageMenuItem(label=_("No icon"), image=Gtk.Image(icon_name="xsi-checkbox-symbolic")) - self.blank_icon_menu_item.connect("activate", self.on_clear_icon_clicked) - menu.add(self.blank_icon_menu_item) - - self.original_icon_menu_image = Gtk.Image() - self.original_icon_menu_item = Gtk.ImageMenuItem(label=_("Use the original icon (if there is one)"), image=self.original_icon_menu_image) - self.original_icon_menu_item.connect("activate", self.on_original_icon_clicked) - menu.add(self.original_icon_menu_item) - - item = Gtk.MenuItem(label=_("Choose...")) - item.connect("activate", self.on_choose_icon_clicked) - menu.add(item) - - menu.show_all() - self.icon_selector_menu_button.set_popup(menu) - - # New row MenuButton - - menu = Gtk.Menu() - - item = Gtk.ImageMenuItem(label=_("New submenu"), image=Gtk.Image(icon_name="xsi-pan-end-symbolic")) - item.connect("activate", self.on_new_submenu_clicked) - menu.add(item) - - item = Gtk.ImageMenuItem(label=_("New separator"), image=Gtk.Image(icon_name="xsi-list-remove-symbolic")) - item.connect("activate", self.on_new_separator_clicked) - menu.add(item) - - menu.show_all() - self.new_row_button.set_popup(menu) - - # Tree/model - - self.path_map = [] - - self.model = Gtk.TreeStore(str, str, str, object) # (hash, uuid, type, Row) - - self.treeview = Gtk.TreeView( - model=self.model, - enable_tree_lines=True, - headers_visible=False, - visible=True - ) - - # Checkbox, Icon and label - column = Gtk.TreeViewColumn() - self.treeview.append_column(column) - column.set_expand(True) - column.set_spacing(2) - - cell = Gtk.CellRendererToggle(activatable=True) - cell.connect("toggled", self.on_action_row_toggled) - column.pack_start(cell, False) - column.set_cell_data_func(cell, self.toggle_render_func) - cell = Gtk.CellRendererPixbuf() - column.pack_start(cell, False) - column.set_cell_data_func(cell, self.menu_icon_render_func) - cell = Gtk.CellRendererText() - column.pack_start(cell, False) - column.set_cell_data_func(cell, self.menu_label_render_func) - - # Accelerators - column = Gtk.TreeViewColumn() - column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - column.set_expand(True) - self.treeview.append_column(column) - - cell = Gtk.CellRendererAccel() - cell.set_property("editable", True) - cell.set_property("xalign", 0) - column.pack_end(cell, False) - column.set_cell_data_func(cell, self.accel_render_func) - - layout = self.treeview.create_pango_layout(_("Click to add a shortcut")) - w, h = layout.get_pixel_size() - column.set_min_width(w + 20) - - cell.connect("editing-started", self.on_accel_edit_started) - cell.connect("accel-edited", self.on_accel_edited) - cell.connect("accel-cleared", self.on_accel_cleared) - self.editing_accel = False - - self.treeview_holder.add(self.treeview) - - self.save_button.connect("clicked", self.on_save_clicked) - self.discard_changes_button.connect("clicked", self.on_discard_changes_clicked) - self.default_layout_button.connect("clicked", self.on_default_layout_clicked) - self.treeview.get_selection().connect("changed", self.on_treeview_position_changed) - self.name_entry.connect("changed", self.on_name_entry_changed) - self.name_entry.connect("icon-press", self.on_name_entry_icon_clicked) - self.remove_submenu_button.connect("clicked", self.on_remove_submenu_clicked) - - self.treeview.connect("row-activated", self.on_row_activated) - - # DND - self.treeview.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, - None, - Gdk.DragAction.MOVE, - ) - self.treeview.drag_dest_set( - Gtk.DestDefaults.ALL, - None, - Gdk.DragAction.MOVE, - ) - - self.treeview.drag_source_add_text_targets() - self.treeview.drag_dest_add_text_targets() - self.treeview.connect("drag-begin", self.on_drag_begin) - self.treeview.connect("drag-end", self.on_drag_begin) - self.treeview.connect("drag-motion", self.on_drag_motion) - self.treeview.connect("drag-data-get", self.on_drag_data_get) - self.treeview.connect("drag-data-received", self.on_drag_data_received) - - self.updating_model = False - self.updating_row_edit_fields = False - self.dnd_autoscroll_timeout_id = 0 - - self.monitors = [] - self.monitor_action_dirs() - - self.needs_saved = False - self.reload_model() - self.update_treeview_state() - self.update_arrow_button_states() - self.set_needs_saved(False) - - def load_nemo_shortcuts(self): - source = Xmlb.BuilderSource() - try: - xml = Gio.resources_lookup_data("/org/nemo/action-layout-editor/nemo-shortcuts.ui", Gio.ResourceLookupFlags.NONE) - ret = source.load_bytes(xml, Xmlb.BuilderSourceFlags.NONE) - builder = Xmlb.Builder() - builder.import_source(source) - silo = builder.compile(Xmlb.BuilderCompileFlags.NONE, None) - except GLib.Error as e: - print("Could not load nemo-shortcuts.ui from resource file - we won't be able to detect built-in shortcut collisions: %s" % e.message) - return - - root = silo.query_first("interface") - for child in root.query(f"object/child", 0): - for section in child.query("object[@class='GtkShortcutsSection']", 0): - for group in section.query("child/object[@class='GtkShortcutsGroup']", 0): - for shortcut in group.query("child/object[@class='GtkShortcutsShortcut']", 0): - label = shortcut.query_text("property[@name='title']") - accel = shortcut.query_text("property[@name='accelerator']") - self.builtin_shortcuts.append(BuiltinShortcut(label, accel)) - - def reload_model(self, flat=False): - self.updating_model = True - self.model.clear() - - if flat: - self.data = { - 'toplevel': [] - } - else: - try: - with open(JSON_FILE, 'r') as file: - try: - self.data = json.load(file) - except json.decoder.JSONDecodeError as e: - print("Could not process json file: %s" % e) - raise - - try: - self.validate_tree(self.data) - except (ValueError, KeyError) as e: - print("Schema validation failed, ignoring saved layout: %s" % e) - raise - except (FileNotFoundError, ValueError, KeyError, json.decoder.JSONDecodeError): - self.data = { - 'toplevel': [] - } - - installed_actions = self.load_installed_actions() - self.fill_model(self.model, None, self.data['toplevel'], installed_actions) - - start_path = Gtk.TreePath.new_first() - self.treeview.get_selection().select_path(start_path) - self.treeview.scroll_to_cell(start_path, None, True, 0, 0) - self.update_row_controls() - - self.updating_model = False - - def monitor_action_dirs (self): - data_dirs = GLib.get_system_data_dirs() + [GLib.get_user_data_dir()] - - for d in data_dirs: - full = os.path.join(d, "nemo", "actions") - file = Gio.File.new_for_path(full) - try: - if not file.query_exists(None): - continue - monitor = file.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES | Gio.FileMonitorFlags.SEND_MOVED, None) - monitor.connect("changed", self.actions_folder_changed) - self.monitors.append(monitor) - except GLib.Error as e: - print("Error monitoring action directory '%s'" % full) - - def actions_folder_changed(self, monitor, file, other, event_type, data=None): - if not file.get_basename().endswith(".nemo_action"): - return - - self.reload_model() - self.update_treeview_state() - self.set_needs_saved(False) - - def save_model(self): - # Save the modified model back to the JSON file - self.data["toplevel"] = self.serialize_model(None, self.model) - - with open(JSON_FILE, 'w') as file: - json.dump(self.data, file, indent=2) - - def validate_tree(self, data): - # Iterate thru every node in the json tree and validate it - for node in data['toplevel']: - self.validate_node(node) - - def validate_node(self, node): - # Check that the node has a valid type - keys = node.keys() - if not ("uuid" in keys and "type" in keys): - raise KeyError("Missing required keys: uuid, type") - - # Mandatory keys - - # Check that the node has a valid UUID - uuid = node['uuid'] - if (not isinstance(uuid, str)) or uuid in (None, ""): - raise ValueError("Invalid or empty UUID '%s' (must not be a non-null, non-empty string)" % str(uuid)) - - # Check that the node has a valid type - type = node['type'] - if (not isinstance(type, str)) or type not in (ROW_TYPE_ACTION, ROW_TYPE_SUBMENU, ROW_TYPE_SEPARATOR): - raise ValueError("%s: Invalid type '%s' (must be a string, either 'action' or 'submenu')" % (uuid, str(node['type']))) - - # Optional keys - - # Check that the node has a valid label - try: - label = node['user-label'] - if (label is not None and (not isinstance(label, str))) or label == "": - raise ValueError("%s: Invalid label '%s' (must be null or a non-zero-length string)" % (uuid, str(label))) - except KeyError: - # not mandatory - pass - - # Check that the node has a valid icon - try: - icon = node['user-icon'] - if icon is not None and (not isinstance(icon, str)): - raise ValueError("%s: Invalid icon '%s' (must be an any-length string or null)" % (uuid, icon)) - except KeyError: - # not mandatory - pass - - # Check the node has a valid accelerator - try: - accel_str = node['accelerator'] - - if accel_str not in ("", None): - key, mods = Gtk.accelerator_parse(accel_str) - if key == 0 and mods == 0: - raise ValueError("%s: Invalid accelerator string '%s'" % (uuid, accel_str)) - except KeyError: - pass - - # Check that the node has a valid children list - try: - children = node['children'] - if node["type"] in (ROW_TYPE_ACTION, ROW_TYPE_SEPARATOR): - print("%s: Action or separator node has children, ignoring them" % uuid) - else: - if not isinstance(children, list): - raise ValueError("%s: Invalid 'children' (must be a list)") - - # Check that the node's children are valid - for child in children: - self.validate_node(child) - except KeyError: - # not mandatory - pass - - def load_installed_actions(self): - # Load installed actions from the system - action_list = [] - - data_dirs = GLib.get_system_data_dirs() + [GLib.get_user_data_dir()] - - for data_dir in data_dirs: - actions_dir = Path(data_dir).joinpath("nemo/actions") - if actions_dir.is_dir(): - for path in actions_dir.iterdir(): - file = Path(path) - if file.suffix == ".nemo_action": - uuid = file.name - - try: - kf = GLib.KeyFile() - kf.load_from_file(str(file), GLib.KeyFileFlags.NONE) - - action_list.append((uuid, file, kf)) - except GLib.Error as e: - print("Error loading action file '%s': %s" % (str(file), e.message)) - continue - - - actions = OrderedDict(sorted((t[0], t[1:]) for t in action_list)) - - return actions - - def fill_model(self, model, parent, items, installed_actions): - disabled_actions = self.nemo_plugin_settings.get_strv("disabled-actions") - scale_factor = self.main_window.get_scale_factor() - - for item in items: - row_type = item.get("type") - uuid = item.get('uuid') - - if row_type == ROW_TYPE_ACTION: - try: - kf = installed_actions[uuid][1] # (path, kf) tuple - path = Path(installed_actions[uuid][0]) - except KeyError: - print("Ignoring missing installed action %s" % uuid) - continue - - iter = model.append(parent, [new_hash(), uuid, row_type, Row(item, kf, path, path.name not in disabled_actions, scale_factor)]) - - del installed_actions[uuid] - elif row_type == ROW_TYPE_SEPARATOR: - iter = model.append(parent, [new_hash(), "separator", ROW_TYPE_SEPARATOR, Row(item, None, None, True)]) - else: - iter = model.append(parent, [new_hash(), uuid, row_type, Row(item, None, None, True)]) - - if 'children' in item: - self.fill_model(model, iter, item['children'], installed_actions) - - # Don't run the following code during recursion, only add untracked actions to the root node - if parent is not None: - return - - def push_disabled(key): - path, kf = installed_actions[key] - return path.name in disabled_actions - - sorted_actions = {uuid: installed_actions[uuid] for uuid in sorted(installed_actions, key=push_disabled)} - - for uuid, (path, kf) in sorted_actions.items(): - enabled = path.name not in disabled_actions - model.append(parent, [new_hash(), uuid, ROW_TYPE_ACTION, Row(None, kf, path, enabled, scale_factor)]) - - def save_disabled_list(self): - disabled = [] - - def get_disabled(model, path, iter, data=None): - row = model.get_value(iter, ROW_OBJ) - row_type = model.get_value(iter, ROW_TYPE) - if row_type == ROW_TYPE_ACTION: - if not row.enabled: - nonlocal disabled - disabled.append(row.get_path().name) - - return False - - self.model.foreach(get_disabled) - - self.nemo_plugin_settings.set_strv("disabled-actions", disabled) - - def on_disabled_settings_list_changed(self, settings, key, data=None): - disabled_actions = self.nemo_plugin_settings.get_strv("disabled-actions") - - def update_disabled(model, path, iter, data=None): - row = model.get_value(iter, ROW_OBJ) - row_uuid = model.get_value(iter, ROW_UUID) - old_enabled = row.enabled - row.enabled = (row_uuid not in disabled_actions) - if old_enabled != row.enabled: - self.model.row_changed(path, iter) - return False - - self.updating_model = True - self.model.foreach(update_disabled) - self.updating_model = False - self.queue_draw() - - def serialize_model(self, parent, model): - used_uuids = {} - result = [] - - iter = model.iter_children(parent) - while iter: - row_type = model.get_value(iter, ROW_TYPE) - row = model.get_value(iter, ROW_OBJ) - raw_uuid = model.get_value(iter, ROW_UUID) - - uuid = raw_uuid - if raw_uuid in used_uuids: - uuid = raw_uuid + str(used_uuids[raw_uuid]) - used_uuids[raw_uuid] += 1 - else: - used_uuids[raw_uuid] = 0 - - item = { - 'uuid': uuid, - 'type': row_type, - 'user-label': row.get_custom_label(), - 'user-icon': row.get_custom_icon(), - 'accelerator': row.get_accelerator_string() - } - - if row_type == ROW_TYPE_SUBMENU: - item['children'] = self.serialize_model(iter, model) - - result.append(item) - iter = model.iter_next(iter) - return result - - def flatten_model(self): - self.reload_model(flat=True) - self.update_treeview_state() - self.set_needs_saved(True) - - def update_treeview_state(self): - self.treeview.expand_all() - - def get_selected_row_path_iter(self): - selection = self.treeview.get_selection() - model, paths = selection.get_selected_rows() - if paths: - path = paths[0] - iter = model.get_iter(path) - return (path, iter) - - return (None, None) - - def set_selection(self, iter): - selection = self.treeview.get_selection() - selection.select_iter(iter) - path, niter = self.get_selected_row_path_iter() - - def get_selected_row_field(self, field): - path, iter = self.get_selected_row_path_iter() - return self.model.get_value(iter, field) - - def selected_row_changed(self, needs_saved=True): - if self.updating_model: - return - - path, iter = self.get_selected_row_path_iter() - if iter is not None: - self.model.row_changed(path, iter) - - self.update_row_controls() - - if needs_saved: - self.set_needs_saved(True) - - def on_treeview_position_changed(self, selection): - if self.updating_model: - return - - self.update_row_controls() - self.update_arrow_button_states() - - def update_row_controls(self): - self.updating_row_edit_fields = True - - try: - row = self.get_selected_row_field(ROW_OBJ) - except TypeError: - self.row_controls_box.set_sensitive(False) - self.name_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None) - self.icon_selector_image.clear() - self.name_entry.set_text("") - return - - self.row_controls_box.set_sensitive(True) - - if row is not None: - row_type = self.get_selected_row_field(ROW_TYPE) - - self.name_entry.set_text(row.get_label()) - - self.set_icon_button(row) - self.original_icon_menu_item.set_visible(row_type == ROW_TYPE_ACTION) - orig_icon = row.get_icon_string(original=True) - self.original_icon_menu_item.set_sensitive(orig_icon is not None and orig_icon != row.get_icon_string()) - self.selected_item_widgets_group.set_sensitive(row.enabled and row_type != ROW_TYPE_SEPARATOR) - self.remove_submenu_button.set_sensitive(row_type in (ROW_TYPE_SUBMENU, ROW_TYPE_SEPARATOR)) - - if row_type == ROW_TYPE_ACTION and row.get_custom_label() is not None: - self.name_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "xsi-edit-delete-symbolic") - self.name_entry.set_icon_sensitive(Gtk.EntryIconPosition.SECONDARY, True) - else: - self.name_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, None) - self.name_entry.set_icon_sensitive(Gtk.EntryIconPosition.SECONDARY, False) - - self.updating_row_edit_fields = False - - def _toggle_row_enabled(self, row): - row.enabled = not row.enabled - self.selected_row_changed(needs_saved=False) - self.save_disabled_list() - - def on_row_activated(self, treeview, path, column, data=None): - if self.updating_row_edit_fields: - return - - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - self._toggle_row_enabled(row) - - def on_action_row_toggled(self, renderer, path, data=None): - iter = self.model.get_iter(path) - row = self.model.get_value(iter, ROW_OBJ) - - if row is not None: - self._toggle_row_enabled(row) - - def set_icon_button(self, row): - for image, use_orig in ([self.icon_selector_image, False], [self.original_icon_menu_image, True]): - - try: - cur_attr, cur_name_or_surface = row.get_icon_type_and_data(original=use_orig) - - if cur_attr == "surface": - image.set_from_surface(cur_name_or_surface) - else: - image.set_from_icon_name(cur_name_or_surface, Gtk.IconSize.BUTTON) - except TypeError: - image.props.icon_name = None - image.props.surface = None - - def set_needs_saved(self, needs_saved): - if needs_saved: - self.save_button.set_sensitive(True) - self.discard_changes_button.set_sensitive(True) - else: - self.save_button.set_sensitive(False) - self.discard_changes_button.set_sensitive(False) - - self.needs_saved = needs_saved - - def update_arrow_button_states(self): - can_up = True - can_down = True - - path, for_iter = self.get_selected_row_path_iter() - first_iter = self.model.get_iter_first() - - if self.same_iter(for_iter, first_iter): - can_up = False - else: - last_iter = None - while first_iter: - last_iter = first_iter - first_iter = self.model.iter_next(first_iter) - - if self.same_iter(for_iter, last_iter): - can_down = False - - self.up_button.set_sensitive(can_up) - self.down_button.set_sensitive(can_down) - - # Button signal handlers - - def up_button_clicked(self, button): - self.move_selection_up_one() - - def down_button_clicked(self, button): - self.move_selection_down_one() +USER_ACTIONS_DIR = os.path.join(GLib.get_user_data_dir(), "nemo", "actions") - def on_save_clicked(self, button): - self.save_model() - self.save_disabled_list() - self.set_needs_saved(False) - - def on_discard_changes_clicked(self, button): - self.set_needs_saved(False) - self.reload_model() - self.update_treeview_state() - - def on_default_layout_clicked(self, button): - self.flatten_model() - - def on_clear_icon_clicked(self, menuitem): - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - row.set_custom_icon("") - self.selected_row_changed() - - def on_original_icon_clicked(self, menuitem): - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - row.set_custom_icon(None) - self.selected_row_changed() - - def on_choose_icon_clicked(self, menuitem): - chooser = XApp.IconChooserDialog() - - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - icon_name = row.get_icon_string() - - if icon_name is not None: - response = chooser.run_with_icon(icon_name) - else: - response = chooser.run() - - if response == Gtk.ResponseType.OK: - row.set_custom_icon(chooser.get_icon_string()) - self.selected_row_changed() - - chooser.hide() - chooser.destroy() - - def on_new_submenu_clicked(self, menuitem): - # Add on same level as current selection - path, selection_iter = self.get_selected_row_path_iter() - row_type = self.get_selected_row_field(ROW_TYPE) - - if row_type == ROW_TYPE_ACTION: - parent = self.model.iter_parent(selection_iter) - else: - parent = selection_iter - - new_iter = self.model.insert_after(parent, selection_iter, [ - new_hash(), - _("New submenu"), - ROW_TYPE_SUBMENU, - Row({"uuid": "New Submenu"}, None, None, True)]) - - selection = self.treeview.get_selection() - selection.select_iter(new_iter) - - self.selected_row_changed() - self.name_entry.grab_focus() - - def on_new_separator_clicked(self, menuitem): - # Add on same level as current selection - path, selection_iter = self.get_selected_row_path_iter() - row_type = self.get_selected_row_field(ROW_TYPE) - - if row_type == ROW_TYPE_ACTION: - parent = self.model.iter_parent(selection_iter) - else: - parent = selection_iter - - new_iter = self.model.insert_after(parent, selection_iter, [ - new_hash(), - "separator", - ROW_TYPE_SEPARATOR, - Row({"uuid": "separator", "type": "separator"}, None, None, True)]) - - selection = self.treeview.get_selection() - selection.select_iter(new_iter) - - self.selected_row_changed() - - def on_remove_submenu_clicked(self, button): - path, selection_iter = self.get_selected_row_path_iter() - row_type = self.model.get_value(selection_iter, ROW_TYPE) - row_hash = self.model.get_value(selection_iter, ROW_HASH) - - if row_type == ROW_TYPE_ACTION: - return - - self.updating_model = True - if row_type == ROW_TYPE_SUBMENU: - parent_iter = self.model.iter_parent(selection_iter) - self.move_tree(self.model, selection_iter, parent_iter) - - self.remove_row_by_hash(self.model, row_hash) - self.updating_model = False - - self.selected_row_changed() - - def on_name_entry_changed(self, entry): - if self.updating_row_edit_fields: - return - - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - row.set_custom_label(entry.get_text()) - - # A submenu's UUID matches its label. Update it when the label is changed. - row_type = self.get_selected_row_field(ROW_TYPE) - if row_type == ROW_TYPE_SUBMENU: - path, iter = self.get_selected_row_path_iter() - if iter is not None: - self.model.set_value(iter, ROW_UUID, entry.get_text()) - - self.selected_row_changed() - - def on_name_entry_icon_clicked(self, entry, icon_pos, event, data=None): - if icon_pos != Gtk.EntryIconPosition.SECONDARY: - return - - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - row.set_custom_label(None) - self.selected_row_changed() - - def on_accel_edited(self, accel, path, key, mods, kc, data=None): - if not self.validate_accelerator(key, mods): - return - - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - row.set_accelerator_string(Gtk.accelerator_name(key, mods)) - self.selected_row_changed() - - def on_accel_cleared(self, accel, path, data=None): - row = self.get_selected_row_field(ROW_OBJ) - if row is not None: - row.set_accelerator_string(None) - self.selected_row_changed() - - def on_accel_edit_started(self, cell, editable, path, data=None): - self.editing_accel = True - editable.connect("editing-done", self.accel_editing_done) - - def accel_editing_done(self, editable, data=None): - self.editing_accel = False - editable.disconnect_by_func(self.accel_editing_done) - - def validate_accelerator(self, key, mods): - # Check nemo's built-ins (copy, paste, etc...) - for shortcut in self.builtin_shortcuts: - if shortcut.key == key and shortcut.mods == mods: - label = f"{shortcut.label}" - dialog = Gtk.MessageDialog( - transient_for=self.main_window, - modal=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=_("This key combination is already in use by Nemo (%s). It cannot be changed.") % label, - use_markup=True - ) - dialog.run() - dialog.destroy() - return False - - conflict = False - - def check_for_action_conflict(iter): - foreach_iter = self.model.iter_children(iter) - - nonlocal conflict - - while not conflict and foreach_iter is not None: - row = self.model.get_value(foreach_iter, ROW_OBJ) - accel_string = row.get_accelerator_string() - - if accel_string is not None: - row_key, row_mod = Gtk.accelerator_parse(accel_string) - if row_key == key and row_mod == mods: - label = f"\n\n{row.get_label()}\n\n" - dialog = Gtk.MessageDialog( - transient_for=self.main_window, - modal=True, - message_type=Gtk.MessageType.WARNING, - buttons=Gtk.ButtonsType.YES_NO, - text=_("This key combination is already in use by another action:") + - label + _("Do you want to replace it?"), - use_markup=True - ) - resp = dialog.run() - dialog.destroy() - - # nonlocal conflict - - if resp == Gtk.ResponseType.YES: - row.set_accelerator_string(None) - conflict = False - else: - conflict = True - break - - foreach_type = self.model.get_value(foreach_iter, ROW_TYPE) - if foreach_type == ROW_TYPE_SUBMENU: - check_for_action_conflict(foreach_iter) - - foreach_iter = self.model.iter_next(foreach_iter) - - check_for_action_conflict(None) - return not conflict - - # Cell render functions - def toggle_render_func(self, column, cell, model, iter, data): - row_type = model.get_value(iter, ROW_TYPE) - row = model.get_value(iter, ROW_OBJ) - - if row_type in (ROW_TYPE_SUBMENU, ROW_TYPE_SEPARATOR): - cell.set_property("visible", False) - else: - cell.set_property("visible", True) - cell.set_property("active", row.enabled) - - def menu_icon_render_func(self, column, cell, model, iter, data): - row = model.get_value(iter, ROW_OBJ) - - try: - attr, name_or_surface = row.get_icon_type_and_data() - cell.set_property(attr, name_or_surface) - except TypeError: - cell.set_property("icon-name", None) - cell.set_property("surface", None) - - def menu_label_render_func(self, column, cell, model, iter, data): - row_type = model.get_value(iter, ROW_TYPE) - row = model.get_value(iter, ROW_OBJ) - - if row_type == ROW_TYPE_SUBMENU: - cell.set_property("markup", "%s" % row.get_label()) - cell.set_property("weight", Pango.Weight.BOLD) - else: - cell.set_property("markup", row.get_label()) - cell.set_property("weight", Pango.Weight.NORMAL if row.enabled else Pango.Weight.ULTRALIGHT) - cell.set_property("style", Pango.Style.NORMAL if row.enabled else Pango.Style.ITALIC) - - def accel_render_func(self, column, cell, model, iter, data): - row_type = model.get_value(iter, ROW_TYPE) - if row_type in (ROW_TYPE_SUBMENU, ROW_TYPE_SEPARATOR): - cell.set_property("visible", False) - return - - row = model.get_value(iter, ROW_OBJ) - - accel_string = row.get_accelerator_string() or "" - key, mods = Gtk.accelerator_parse(accel_string) - cell.set_property("visible", True) - cell.set_property("accel-key", key) - cell.set_property("accel-mods", mods) - - if accel_string == "": - spath, siter = self.get_selected_row_path_iter() - current_path = model.get_path(iter) - if current_path is not None and current_path.compare(spath) == 0: - if not self.editing_accel: - cell.set_property("text", _("Click to add a shortcut")) - else: - cell.set_property("text", None) - else: - cell.set_property("text", " ") - - # DND - - def on_drag_begin(self, widget, context): - source_path, source_iter = self.get_selected_row_path_iter() - width = 0 - height = 0 - - def gather_row_surfaces(current_root_iter, surfaces): - foreach_iter = self.model.iter_children(current_root_iter) - - while foreach_iter is not None: - foreach_path = self.model.get_path(foreach_iter) - surface = self.treeview.create_row_drag_icon(foreach_path) - row_surfaces.append(surface) - nonlocal width - nonlocal height - width = max(width, surface.get_width()) - height += surface.get_height() - 1 - - foreach_type = self.model.get_value(foreach_iter, ROW_TYPE) - if foreach_type == ROW_TYPE_SUBMENU: - gather_row_surfaces(foreach_iter, surfaces) - - foreach_iter = self.model.iter_next(foreach_iter) - - source_row_surface = self.treeview.create_row_drag_icon(source_path) - width = source_row_surface.get_width() - height = source_row_surface.get_height() - 1 - row_surfaces = [source_row_surface] - - gather_row_surfaces(source_iter, row_surfaces) - - final_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width + 4, height + 4) - scale = self.main_window.get_scale_factor() - final_surface.set_device_scale(scale, scale) - - def sc(v): - return v * scale - - def usc(v): - return v / scale - - cr = cairo.Context(final_surface) - - y = 2 - first = True - for s in row_surfaces: - cr.save() - cr.set_source_surface(s, 2, y) - cr.paint() - cr.restore() - if not first: - cr.save() - cr.set_source_rgb(1, 1, 1) - cr.rectangle(sc(1), y - 2, usc(width) - 2, 4) - cr.fill() - cr.restore() - first = False - - y += usc(s.get_height()) - 1 - - cr.show_page() - - Gtk.drag_set_icon_surface(context, final_surface) - - def on_drag_end(self, context, data=None): - self.dnd_autoscroll_cancel() - - def dnd_autoscroll(self): - AUTO_SCROLL_MARGIN = 20 - - window = self.treeview.get_bin_window() - vadjust = self.treeview_holder.get_vadjustment() - seat = Gdk.Display.get_default().get_default_seat() - pointer = seat.get_pointer() - - window, x, y, mask = window.get_device_position(pointer) - y += vadjust.get_value() - rect = self.treeview.get_visible_rect() - - offset = y - (rect.y + 2 * AUTO_SCROLL_MARGIN) - if offset > 0: - offset = y - (rect.y + rect.height - 2 * AUTO_SCROLL_MARGIN); - if offset < 0: - return - - value = max(0.0, min(vadjust.get_value() + offset, vadjust.get_upper() - vadjust.get_page_size())) - vadjust.set_value(value) - - self.dnd_autoscroll_start() - - def dnd_autoscroll_timeout(self, data=None): - self.dnd_autoscroll_timeout_id = 0 - self.dnd_autoscroll() - - return GLib.SOURCE_REMOVE - - def dnd_autoscroll_cancel(self): - if self.dnd_autoscroll_timeout_id > 0: - GLib.source_remove(self.dnd_autoscroll_timeout_id) - self.dnd_autoscroll_timeout_id = 0 - - def dnd_autoscroll_start(self): - if self.dnd_autoscroll_timeout_id > 0: - GLib.source_remove(self.dnd_autoscroll_timeout_id) - self.dnd_autoscroll_timeout_id = GLib.timeout_add(50, self.dnd_autoscroll_timeout) - - def on_drag_motion(self, widget, context, x, y, etime): - target_row = self.treeview.get_dest_row_at_pos(x, y) - if not target_row: - Gdk.drag_status(context, 0, etime) - return False - - model = self.treeview.get_model() - - path, position = target_row - i = model.get_iter(path) - target_row_type = model.get_value(i, ROW_TYPE) - target_row = model.get_value(i, ROW_OBJ) - source_path, source_iter = self.get_selected_row_path_iter() - source_row_type = model.get_value(source_iter, ROW_TYPE) - - if source_path.compare(path) == 0 or source_path.is_ancestor(path) and source_row_type == ROW_TYPE_SUBMENU: - Gdk.drag_status(context, 0, etime) - return False - - if target_row_type != ROW_TYPE_SUBMENU and position in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, - Gtk.TreeViewDropPosition.INTO_OR_AFTER): - Gdk.drag_status(context, 0, etime) - return False - - self.treeview.set_drag_dest_row(path, position) - action = Gdk.DragAction.MOVE - - self.dnd_autoscroll_start() - - Gdk.drag_status(context, action, etime) - return True - - def on_drag_data_get(self, widget, context, selection_data, info, etime): - target_atom = selection_data.get_target() - target = target_atom.name() - - if target == "UTF8_STRING": - selection = self.treeview.get_selection() - model, paths = selection.get_selected_rows() - if paths: - path = paths[0] - iter = model.get_iter(path) - item_data = { - "hash": model.get_value(iter, ROW_HASH), - "uuid": model.get_value(iter, ROW_UUID), - 'type': model.get_value(iter, ROW_TYPE) - } - - selection_data.set_text(json.dumps(item_data), -1) - - def on_drag_data_received(self, widget, context, x, y, selection_data, info, etime): - drop_info = self.treeview.get_dest_row_at_pos(x, y) - if not drop_info: - Gdk.drag_status(context, 0, etime) - return - - path, position = drop_info - - if selection_data: - dropped_data = selection_data.get_text() - - if path: - iter = self.model.get_iter(path) - parent = self.model.iter_parent(iter) - else: - iter = None - parent = None - - if not self.reorder_items(iter, parent, dropped_data, position): - Gdk.drag_status(context, 0, etime) - return - Gtk.drag_finish(context, True, True, etime) - - self.set_needs_saved(True) - self.update_treeview_state() - - def reorder_items(self, target_iter, parent, dropped_data, position): - source_data = json.loads(dropped_data) - source_hash = source_data['hash'] - source_uuid = source_data['uuid'] - source_type = source_data['type'] - source_iter = self.lookup_iter_by_hash(self.model, source_hash) - - if source_iter is None: - print("no source row found, cancelling drop") - return False - - target_row_type = self.model.get_value(target_iter, ROW_TYPE) - if target_row_type == ROW_TYPE_ACTION and \ - position in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, - Gtk.TreeViewDropPosition.INTO_OR_AFTER): - return False - - new_iter = None - row = self.model.get_value(source_iter, ROW_OBJ) - - if target_row_type == ROW_TYPE_SUBMENU and \ - position in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, - Gtk.TreeViewDropPosition.INTO_OR_AFTER, - Gtk.TreeViewDropPosition.AFTER): - new_iter = self.model.insert(target_iter, 0, [new_hash(), source_uuid, source_type, row]) - else: - if position == Gtk.TreeViewDropPosition.BEFORE: - new_iter = self.model.insert_before(parent, target_iter, [new_hash(), source_uuid, source_type, row]) - elif position == Gtk.TreeViewDropPosition.AFTER: - new_iter = self.model.insert_after(parent, target_iter, [new_hash(), source_uuid, source_type, row]) - - # we have to recreate all children to the new menu location. - if new_iter is not None: - if source_type == ROW_TYPE_SUBMENU: - self.move_tree(self.model, source_iter, new_iter) - self.remove_row_by_hash(self.model, source_hash) - - return True +class EditorWindow(): + def __init__(self): + self.main_window = Gtk.Window() + self.main_window.set_default_size(800, 600) + self.main_window.set_icon_name("nemo") - # Up/Down button handling - def get_new_row_data(self, source_iter): - source_hash = self.model.get_value(source_iter, ROW_HASH) - source_uuid = self.model.get_value(source_iter, ROW_UUID) - source_type = self.model.get_value(source_iter, ROW_TYPE) - source_object = self.model.get_value(source_iter, ROW_OBJ) - target_hash = new_hash() + header = Gtk.HeaderBar() + header.set_show_close_button(True) + header.set_title(_("Nemo Actions Layout Editor")) + self.main_window.set_titlebar(header) - return ( - source_hash, - target_hash, - source_type, - [ - target_hash, - source_uuid, - source_type, - source_object - ] + self.hamburger_button = Gtk.MenuButton( + image=Gtk.Image.new_from_icon_name("xsi-open-menu-symbolic", Gtk.IconSize.BUTTON) ) + header.pack_start(self.hamburger_button) - def get_last_at_level(self, model, iter): - if model.iter_has_child(iter): - foreach_iter = model.iter_children(iter) - while foreach_iter is not None: - last_iter = foreach_iter - foreach_iter = model.iter_next(foreach_iter) - continue - - return self.get_last_at_level(model, last_iter) - return iter - - def same_iter(self, iter1, iter2): - if iter1 is None and iter2 is None: - return True - elif iter1 is None and iter2 is not None: - return False - elif iter2 is None and iter1 is not None: - return False - - path1 = self.model.get_path(iter1) - path2 = self.model.get_path(iter2) - return path1.compare(path2) == 0 - - def path_is_valid(self, path): - try: - test_iter = self.model.get_iter(path) - except ValueError: - return False - return True - - def next_path_validated(self, path): - path.next() - return self.path_is_valid(path) - - """ - The move_selection_up_one and _down_one methods are complicated because there are only up/down - arrows (to keep things simple to the user). This navigates the treeview as if it was fully - expanded, but movement is by row, *not* by level, so it's possible to reach every node in the - tree. - """ - - def move_selection_up_one(self): - path, iter = self.get_selected_row_path_iter() - if iter is None: - return - - parent = self.model.iter_parent(iter) - - target_path = path - target_iter = None - target_parent = None - - source_hash, target_hash, row_type, inserted_row = self.get_new_row_data(iter) - inserted_iter = None - - if target_path.prev(): - target_iter = self.get_last_at_level(self.model, self.model.get_iter(target_path)) - target_iter_type = self.model.get_value(target_iter, ROW_TYPE) - - if target_iter_type == ROW_TYPE_SUBMENU: - inserted_iter = self.model.prepend(target_iter, inserted_row) - target_parent = target_iter - else: - target_parent = self.model.iter_parent(target_iter) - - if self.same_iter(parent, target_parent): - inserted_iter = self.model.insert_before(target_parent, target_iter, inserted_row) - else: - inserted_iter = self.model.insert_after(target_parent, target_iter, inserted_row) - elif target_path.up(): - if target_path.prev(): - target_iter = self.get_last_at_level(self.model, self.model.get_iter(target_path)) - target_parent = self.model.iter_parent(target_iter) - inserted_iter = self.model.insert_after(target_parent, target_iter, inserted_row) - else: - # We're at the top? - top_iter = self.model.get_iter_first() - if not self.same_iter(iter, top_iter): - inserted_iter = self.model.insert_before(None, top_iter, inserted_row) - - source_was_expanded = False - - if inserted_iter is not None: - self.updating_model = True - - if row_type == ROW_TYPE_SUBMENU: - if self.treeview.row_expanded(self.model.get_path(iter)): - source_was_expanded = True - self.move_tree(self.model, iter, inserted_iter) - - if target_parent is not None: - self.treeview.expand_row(self.model.get_path(target_parent), True) - elif source_was_expanded: - self.treeview.expand_row(self.model.get_path(inserted_iter), True) - - self.remove_row_by_hash(self.model, source_hash) - self.updating_model = False - - self.select_row_by_hash(self.model, target_hash) - - self.treeview.scroll_to_cell(self.model.get_path(inserted_iter), None, False, 0, 0) - self.set_needs_saved(True) - - def move_selection_down_one(self): - path, iter = self.get_selected_row_path_iter() - - if iter is None: - return - - target_path = path - target_iter = None - target_parent = None - - source_hash, target_hash, row_type, inserted_row = self.get_new_row_data(iter) - inserted_iter = None - - if self.next_path_validated(target_path): - # is it a menu? Add it as its first child - maybe_submenu_iter = self.model.get_iter(target_path) - maybe_submenu_type = self.model.get_value(maybe_submenu_iter, ROW_TYPE) - if maybe_submenu_type == ROW_TYPE_SUBMENU: - inserted_iter = self.model.prepend(maybe_submenu_iter, inserted_row) - target_parent = maybe_submenu_iter - else: - # or else add after the test row - target_iter = self.model.get_iter(target_path) - target_parent = self.model.iter_parent(target_iter) - inserted_iter = self.model.insert_after(target_parent, target_iter, inserted_row) - else: - # path_next_validated modifies target_path directly, reset it to the origin - target_path = path - if target_path.get_depth() > 1 and target_path.up() and self.path_is_valid(target_path): - target_iter = self.model.get_iter(target_path) - target_parent = self.model.iter_parent(target_iter) - inserted_iter = self.model.insert_after(target_parent, target_iter, inserted_row) - - source_was_expanded = False - - if inserted_iter is not None: - self.updating_model = True - - if row_type == ROW_TYPE_SUBMENU: - if self.treeview.row_expanded(self.model.get_path(iter)): - source_was_expanded = True - self.move_tree(self.model, iter, inserted_iter) - - if target_parent is not None: - self.treeview.expand_row(self.model.get_path(target_parent), True) - elif source_was_expanded: - self.treeview.expand_row(self.model.get_path(inserted_iter), True) - - self.remove_row_by_hash(self.model, source_hash) - self.updating_model = False - - self.select_row_by_hash(self.model, target_hash) - - self.treeview.scroll_to_cell(self.model.get_path(inserted_iter), None, False, 0, 0) - self.set_needs_saved(True) - - def move_tree(self, model, source_iter, new_iter): - foreach_iter = self.model.iter_children(source_iter) - - while foreach_iter is not None: - row_hash = model.get_value(foreach_iter, ROW_HASH) - row_uuid = model.get_value(foreach_iter, ROW_UUID) - row_type = model.get_value(foreach_iter, ROW_TYPE) - row = model.get_value(foreach_iter, ROW_OBJ) - - if row is None: - print("During prune/paste, could not find row for %s with hash %s" % (row_uuid, row_hash)) - continue - inserted_iter = self.model.insert(new_iter, -1, [ - new_hash(), - row_uuid, - row_type, - row - ]) - - if row_type == ROW_TYPE_SUBMENU: - self.move_tree(model, foreach_iter, inserted_iter) - - foreach_iter = self.model.iter_next(foreach_iter) - - def lookup_iter_by_hash(self, model, hash): - result = None - - def compare(model, path, iter, data): - current = model.get_value(iter, ROW_HASH) - if current == hash: - nonlocal result - result = iter - return True - return False - - model.foreach(compare, hash) - return result - - def remove_row_by_hash(self, model, old_hash): - iter = self.lookup_iter_by_hash(model, old_hash) - - if iter is not None: - model.remove(iter) - - def select_row_by_hash(self, model, hash): - iter = self.lookup_iter_by_hash(model, hash) - - if iter is not None: - self.set_selection(iter) - - def quit(self, *args, **kwargs): - if self.needs_saved: - dialog = Gtk.MessageDialog( - transient_for=self.main_window, - modal=True, - message_type=Gtk.MessageType.OTHER, - buttons=Gtk.ButtonsType.YES_NO, - text=_("The layout has changed. Save it?") - ) - - dialog.set_title(_("Unsaved changes")) - response = dialog.run() - dialog.destroy() - - if response == Gtk.ResponseType.DELETE_EVENT: - return False - - if response == Gtk.ResponseType.YES: - self.save_model() - self.save_disabled_list() - - for monitor in self.monitors: - monitor.cancel() - - return True - -class EditorWindow(): - def __init__(self): - self.builder = Gtk.Builder.new_from_resource("/org/nemo/action-layout-editor/nemo-action-layout-editor.glade") - self.main_window = self.builder.get_object("main_window") - self.hamburger_button = self.builder.get_object("hamburger_button") - self.editor = NemoActionsOrganizer(self.main_window, self.builder) - self.main_window.add(self.editor) - - # Hamburger menu menu = Gtk.Menu() - - item = Gtk.ImageMenuItem(label=_("Open user actions folder"), image=Gtk.Image(icon_name="xsi-folder-symbolic", icon_size=Gtk.IconSize.MENU)) + item = Gtk.ImageMenuItem( + label=_("Open user actions folder"), + image=Gtk.Image(icon_name="xsi-folder-symbolic", icon_size=Gtk.IconSize.MENU) + ) item.connect("activate", self.open_actions_folder_clicked) menu.add(item) + item = Gtk.SeparatorMenuItem() + menu.add(item) + item = Gtk.MenuItem(label=_("Quit")) item.connect("activate", self.quit) menu.add(item) @@ -1547,23 +51,28 @@ def __init__(self): menu.show_all() self.hamburger_button.set_popup(menu) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, border_width=8) + self.main_window.add(vbox) + + self.editor = Nemo.ActionLayoutEditor.new() + vbox.pack_start(self.editor, True, True, 0) + self.main_window.connect("delete-event", self.window_delete) self.main_window.show_all() self.main_window.present_with_time(0) def quit(self, button): - if self.editor.quit(): - Gtk.main_quit() - return True - return False + Gtk.main_quit() + return True def window_delete(self, window, event, data=None): - if self.editor.quit(): - Gtk.main_quit() - return Gdk.EVENT_STOP + Gtk.main_quit() + return False def open_actions_folder_clicked(self, button): + # Create actions directory if it doesn't exist + os.makedirs(USER_ACTIONS_DIR, exist_ok=True) subprocess.Popen(["xdg-open", USER_ACTIONS_DIR]) if __name__ == "__main__": @@ -1572,4 +81,4 @@ def open_actions_folder_clicked(self, button): EditorWindow() Gtk.main() - sys.exit(0) \ No newline at end of file + sys.exit(0) diff --git a/debian/libnemo-extension1.symbols b/debian/libnemo-extension1.symbols index de264de0f..cd7a8b1bc 100644 --- a/debian/libnemo-extension1.symbols +++ b/debian/libnemo-extension1.symbols @@ -1,4 +1,6 @@ libnemo-extension.so.1 libnemo-extension1 #MINVER# + nemo_action_layout_editor_get_type@Base 6.6.1 + nemo_action_layout_editor_new@Base 6.6.1 nemo_column_get_type@Base 5.0.3 nemo_column_new2@Base 5.0.3 nemo_column_new@Base 5.0.3 diff --git a/gresources/meson.build b/gresources/meson.build index b89ed6225..a3d978ad5 100644 --- a/gresources/meson.build +++ b/gresources/meson.build @@ -5,11 +5,3 @@ gresources = gnome.compile_resources( install_header: false, install: false ) - -layout_editor_gresources = gnome.compile_resources( - 'nemo-action-layout-editor-resources', 'nemo-action-layout-editor.gresource.xml', - source_dir: '.', - gresource_bundle: true, - install: true, - install_dir: nemoDataPath -) diff --git a/gresources/nemo-action-layout-editor.glade b/gresources/nemo-action-layout-editor.glade index 5c0092476..b69fd72f1 100644 --- a/gresources/nemo-action-layout-editor.glade +++ b/gresources/nemo-action-layout-editor.glade @@ -60,13 +60,19 @@ vertical 4 - + True - True + False True in - + + True + False + + + + @@ -111,6 +117,9 @@ True Remove image2 + False @@ -286,7 +295,7 @@ True False 4 - 6 + True Save @@ -306,7 +315,7 @@ - Cancel + Cancel changes True True True @@ -331,6 +340,9 @@ 2 + False @@ -347,39 +359,4 @@ - - False - 4 - center - 600 - 400 - nemo - - - - - - True - False - Nemo Actions Layout Editor - False - True - - - True - True - False - True - - - True - False - xsi-open-menu-symbolic - - - - - - - diff --git a/gresources/nemo-file-management-properties.glade b/gresources/nemo-file-management-properties.glade index 02b8e4cb4..3aad0c48c 100644 --- a/gresources/nemo-file-management-properties.glade +++ b/gresources/nemo-file-management-properties.glade @@ -379,6 +379,7 @@ along with . If not, see . False File Management Preferences + True center 800 600 @@ -4354,6 +4355,36 @@ along with . If not, see . 7 + + + True + True + 6 + + + True + False + + + True + False + 4 + vertical + + + + + + + + + + actions + Actions + xsi-open-menu-symbolic + 8 + + True @@ -4364,7 +4395,7 @@ along with . If not, see . True False - + True False vertical @@ -4378,9 +4409,9 @@ along with . If not, see . plugins - Plugins + Extensions xapp-prefs-plugins-symbolic - 8 + 9 diff --git a/gresources/nemo.gresource.xml b/gresources/nemo.gresource.xml index 7d0870e7f..14af39bcb 100644 --- a/gresources/nemo.gresource.xml +++ b/gresources/nemo.gresource.xml @@ -27,4 +27,7 @@ nemo-style-fallback-mandatory.css nemo-style-application.css + + nemo-action-layout-editor.glade + diff --git a/libnemo-extension/meson.build b/libnemo-extension/meson.build index 5b589d32a..cee7a71ee 100644 --- a/libnemo-extension/meson.build +++ b/libnemo-extension/meson.build @@ -5,6 +5,7 @@ nemo_i18n_header = [ nemo_extension_sources = [ gresources, + 'nemo-action-layout-editor.c', 'nemo-column-provider.c', 'nemo-column.c', 'nemo-desktop-preferences.c', @@ -22,6 +23,7 @@ nemo_extension_sources = [ ] nemo_extension_headers = [ + 'nemo-action-layout-editor.h', 'nemo-column-provider.h', 'nemo-column.h', 'nemo-desktop-preferences.h', @@ -40,7 +42,7 @@ nemo_extension_headers = [ nemo_extension_incdir = include_directories('.') -nemo_extension_deps = [ glib, gtk ] +nemo_extension_deps = [ glib, gtk, json, xapp ] nemo_extension_lib = shared_library('nemo-extension', nemo_extension_sources + nemo_i18n_header, @@ -86,3 +88,13 @@ pkgconfig.generate(filebase: 'libnemo-extension', variables: 'extensiondir=${libdir}/@0@/@1@'.format('nemo', 'extensions-3.0'), ) +# Test executable for NemoActionLayoutEditor +test_action_layout_editor = executable('test-action-layout-editor', + 'test-action-layout-editor.c', + dependencies: nemo_extension_deps, + link_with: nemo_extension_lib, + include_directories: [ rootInclude, nemo_extension_incdir ], + build_by_default: true, + install: false, +) + diff --git a/libnemo-extension/nemo-action-layout-editor.c b/libnemo-extension/nemo-action-layout-editor.c new file mode 100644 index 000000000..0da29f4c0 --- /dev/null +++ b/libnemo-extension/nemo-action-layout-editor.c @@ -0,0 +1,2712 @@ +/* nemo-action-layout-editor.c */ + +#include +#include "nemo-action-layout-editor.h" +#include +#include +#include +#include +#include +#include + +#define DEBUG_EDITOR 0 + +#if DEBUG_EDITOR +#define DEBUG(format, ...) g_print("ActionLayoutEditor: " format "\n", ##__VA_ARGS__) +#else +#define DEBUG(format, ...) G_STMT_START { } G_STMT_END +#endif + +#define JSON_FILE "nemo/actions-tree.json" +#define USER_ACTIONS_DIR "nemo/actions" + +// From nemo-action-symbols.h +#define ACTION_FILE_GROUP "Nemo Action" +#define KEY_NAME "Name" +#define KEY_ICON_NAME "Icon-Name" +#define NEMO_PLUGIN_PREFERENCES_DISABLED_ACTIONS "disabled-actions" + +enum { + COL_HASH, + COL_UUID, + COL_TYPE, + COL_ROW_DATA, + N_COLUMNS +}; + +typedef enum { + ROW_TYPE_ACTION, + ROW_TYPE_SUBMENU, + ROW_TYPE_SEPARATOR +} RowType; + +typedef struct { + gchar *label; + guint key; + GdkModifierType mods; +} BuiltinShortcut; + +typedef struct { + gchar *uuid; + RowType type; + gchar *user_label; + gchar *user_icon; + gchar *accelerator; + gchar *filename; // only used for NemoActions + GKeyFile *keyfile; // same + gboolean enabled; +} RowData; + +static const gchar * +row_type_to_string (RowType type) +{ + switch (type) { + case ROW_TYPE_ACTION: + return "action"; + case ROW_TYPE_SUBMENU: + return "submenu"; + case ROW_TYPE_SEPARATOR: + return "separator"; + default: + return "action"; + } +} + +static RowType +row_type_from_string (const gchar *str) +{ + if (g_strcmp0 (str, "submenu") == 0) + return ROW_TYPE_SUBMENU; + else if (g_strcmp0 (str, "separator") == 0) + return ROW_TYPE_SEPARATOR; + else + return ROW_TYPE_ACTION; +} + +static void row_data_free (RowData *data); + +static RowData * +row_data_copy (RowData *data) +{ + RowData *copy; + + if (data == NULL) + return NULL; + + copy = g_new0 (RowData, 1); + copy->uuid = g_strdup (data->uuid); + copy->type = data->type; + copy->user_label = g_strdup (data->user_label); + copy->user_icon = g_strdup (data->user_icon); + copy->accelerator = g_strdup (data->accelerator); + copy->filename = g_strdup (data->filename); + copy->enabled = data->enabled; + + if (data->keyfile) + copy->keyfile = g_key_file_ref (data->keyfile); + + return copy; +} + +static GType +row_data_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) { + type = g_boxed_type_register_static ("NemoActionLayoutEditorRowData", + (GBoxedCopyFunc) row_data_copy, + (GBoxedFreeFunc) row_data_free); + } + + return type; +} + +#define ROW_DATA_TYPE (row_data_get_type ()) + +typedef struct { + /* UI widgets */ + GtkWidget *treeview; + GtkTreeStore *model; + GtkWidget *save_button; + GtkWidget *discard_button; + GtkWidget *default_layout_button; + GtkWidget *name_entry; + GtkWidget *new_row_button; + GtkWidget *row_controls_box; + GtkWidget *remove_submenu_button; + GtkWidget *icon_selector_menu_button; + GtkWidget *icon_selector_image; + GtkWidget *original_icon_menu_image; + GtkWidget *original_icon_menu_item; + GtkWidget *up_button; + GtkWidget *down_button; + GtkWidget *scrolled_window; + + XAppVisibilityGroup *selected_item_widgets_group; + + /* Settings and monitors */ + GSettings *nemo_plugin_settings; + GList *dir_monitors; + gulong settings_handler_id; + + GList *builtin_shortcuts; + + /* State */ + gboolean needs_saved; + gboolean updating_model; + gboolean updating_row_edit_fields; + gboolean editing_accel; + + /* DND */ + guint dnd_autoscroll_timeout_id; +} NemoActionLayoutEditorPrivate; + +struct _NemoActionLayoutEditor +{ + GtkBox parent_instance; + NemoActionLayoutEditorPrivate *priv; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (NemoActionLayoutEditor, nemo_action_layout_editor, GTK_TYPE_BOX) + +static void reload_model (NemoActionLayoutEditor *self, gboolean flat); +static void save_model (NemoActionLayoutEditor *self); +static void set_needs_saved (NemoActionLayoutEditor *self, gboolean needs_saved); +static void update_row_controls (NemoActionLayoutEditor *self); +static void update_arrow_button_states (NemoActionLayoutEditor *self); +static void selected_row_changed (NemoActionLayoutEditor *self, gboolean needs_saved); +static gboolean lookup_iter_by_hash (GtkTreeModel *model, const gchar *hash, GtkTreeIter *result); +static void remove_row_by_hash (NemoActionLayoutEditor *self, const gchar *hash); +static void select_row_by_hash (NemoActionLayoutEditor *self, const gchar *hash); +static void move_tree_recursive (NemoActionLayoutEditor *self, GtkTreeIter *source_iter, GtkTreeIter *new_parent); + +static RowData * +row_data_new (void) +{ + RowData *data = g_new0 (RowData, 1); + data->enabled = TRUE; + return data; +} + +static void +row_data_free (RowData *data) +{ + if (data == NULL) + return; + + g_free (data->uuid); + g_free (data->user_label); + g_free (data->user_icon); + g_free (data->accelerator); + g_free (data->filename); + + if (data->keyfile) + g_key_file_unref (data->keyfile); + + g_free (data); +} + +static void +tree_store_append_row_data (GtkTreeStore *model, + GtkTreeIter *iter, + GtkTreeIter *parent, + RowData *data) +{ + g_autofree gchar *hash = g_uuid_string_random (); + + gtk_tree_store_append (model, iter, parent); + gtk_tree_store_set (model, iter, + COL_HASH, hash, + COL_UUID, data->uuid, + COL_TYPE, data->type, + COL_ROW_DATA, data, + -1); + row_data_free (data); +} + +static void +tree_store_update_row_data (GtkTreeStore *model, + GtkTreeIter *iter, + RowData *data) +{ + gtk_tree_store_set (model, iter, + COL_ROW_DATA, data, + -1); + row_data_free (data); +} + +static BuiltinShortcut * +builtin_shortcut_new (const gchar *label, + const gchar *accelerator) +{ + BuiltinShortcut *shortcut = g_new0 (BuiltinShortcut, 1); + shortcut->label = g_strdup (label); + + gtk_accelerator_parse (accelerator, &shortcut->key, &shortcut->mods); + return shortcut; +} + +static void +builtin_shortcut_free (BuiltinShortcut *shortcut) +{ + if (shortcut == NULL) + return; + + g_free (shortcut->label); + g_free (shortcut); +} + +static gchar * +row_data_get_icon_string (RowData *data, gboolean original) +{ + if (!original && data->user_icon != NULL) + return g_strdup (data->user_icon); + + if (data->keyfile != NULL) { + return g_key_file_get_string (data->keyfile, + ACTION_FILE_GROUP, + KEY_ICON_NAME, + NULL); + } + + return NULL; +} + +static gchar * +row_data_get_label (RowData *data) +{ + if (data->type == ROW_TYPE_SEPARATOR) + return g_strdup ("──────────────────────────────"); + + if (data->user_label != NULL) + return g_strdup (data->user_label); + + if (data->keyfile != NULL) { + gchar *label = g_key_file_get_locale_string (data->keyfile, + ACTION_FILE_GROUP, + KEY_NAME, + NULL, + NULL); + if (label) { + gchar *result = g_strdup (label); + g_free (label); + /* Remove underscores */ + gchar *p = result; + while (*p) { + if (*p == '_') + *p = ' '; + p++; + } + return result; + } + } + + return g_strdup (_("Unknown")); +} + +static gboolean +get_selected_row (NemoActionLayoutEditor *self, GtkTreePath **path_out, GtkTreeIter *iter_out) +{ + GtkTreeSelection *selection; + GList *paths; + gboolean result = FALSE; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->treeview)); + paths = gtk_tree_selection_get_selected_rows (selection, NULL); + + if (paths) { + GtkTreePath *path = paths->data; + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), iter_out, path)) { + if (path_out) + *path_out = gtk_tree_path_copy (path); + result = TRUE; + } + g_list_free_full (paths, (GDestroyNotify) gtk_tree_path_free); + } + + return result; +} + +static RowData * +get_selected_row_data (NemoActionLayoutEditor *self, GtkTreeIter *iter_out) +{ + GtkTreeIter iter; + RowData *data = NULL; + + if (get_selected_row (self, NULL, &iter)) { + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_ROW_DATA, &data, + -1); + } + + if (iter_out) + *iter_out = iter; + + return data; +} + +static void +save_disabled_list (NemoActionLayoutEditor *self) +{ + GStrvBuilder *builder = g_strv_builder_new (); + GtkTreeIter iter; + gboolean valid; + gchar **disabled_actions; + + valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->model), &iter); + + while (valid) { + RowType row_type; + RowData *data; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_TYPE, &row_type, + COL_ROW_DATA, &data, + -1); + + if (row_type == ROW_TYPE_ACTION) { + if (!data->enabled && data->filename) { + gchar *basename = g_path_get_basename (data->filename); + g_strv_builder_add (builder, basename); + g_free (basename); + } + } + + if (data != NULL) { + row_data_free (data); + } + + valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &iter); + } + + disabled_actions = g_strv_builder_end (builder); + + g_signal_handler_block (self->priv->nemo_plugin_settings, self->priv->settings_handler_id); + g_settings_set_strv (self->priv->nemo_plugin_settings, + NEMO_PLUGIN_PREFERENCES_DISABLED_ACTIONS, + (const gchar * const *) disabled_actions); + g_signal_handler_unblock (self->priv->nemo_plugin_settings, self->priv->settings_handler_id); + + g_strfreev (disabled_actions); + g_strv_builder_unref (builder); +} + +static gboolean +update_row_disabled_state (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + gchar **disabled_actions = (gchar **) user_data; + RowType row_type; + RowData *data; + gboolean old_enabled, new_enabled; + + gtk_tree_model_get (model, iter, + COL_TYPE, &row_type, + -1); + + if (row_type != ROW_TYPE_ACTION || !data) + return FALSE; + + gtk_tree_model_get (model, iter, + COL_ROW_DATA, &data, + -1); + + if (data == NULL) { + return FALSE; + } + + old_enabled = data->enabled; + + new_enabled = TRUE; + if (disabled_actions) { + for (gint i = 0; disabled_actions[i] != NULL; i++) { + if (g_strcmp0 (data->uuid, disabled_actions[i]) == 0) { + new_enabled = FALSE; + break; + } + } + } + + if (old_enabled != new_enabled) { + data->enabled = new_enabled; + tree_store_update_row_data (GTK_TREE_STORE (model), iter, data); + gtk_tree_model_row_changed (model, path, iter); + } else { + row_data_free (data); + + } + + return FALSE; +} + +static void +on_disabled_settings_list_changed (GSettings *settings, + const gchar *key, + NemoActionLayoutEditor *self) +{ + gchar **disabled_actions; + + if (g_strcmp0 (key, NEMO_PLUGIN_PREFERENCES_DISABLED_ACTIONS) != 0) + return; + + disabled_actions = g_settings_get_strv (self->priv->nemo_plugin_settings, + NEMO_PLUGIN_PREFERENCES_DISABLED_ACTIONS); + + gtk_tree_model_foreach (GTK_TREE_MODEL (self->priv->model), + update_row_disabled_state, + disabled_actions); + + g_strfreev (disabled_actions); +} + +static void +on_save_clicked (GtkButton *button, NemoActionLayoutEditor *self) +{ + save_model (self); + save_disabled_list (self); + set_needs_saved (self, FALSE); +} + +static void +on_discard_changes_clicked (GtkButton *button, NemoActionLayoutEditor *self) +{ + reload_model (self, FALSE); + set_needs_saved (self, FALSE); +} + +static void +on_default_layout_clicked (GtkButton *button, NemoActionLayoutEditor *self) +{ + reload_model (self, TRUE); + set_needs_saved (self, TRUE); +} + +static void +insert_new_row_at_selection (NemoActionLayoutEditor *self, + RowType type, + const gchar *default_label, + gboolean grab_name_focus) +{ + GtkTreeIter iter, new_iter, parent_iter; + GtkTreePath *path; + RowType row_type; + RowData *data; + + if (!get_selected_row (self, &path, &iter)) + return; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_TYPE, &row_type, + -1); + + if (row_type == ROW_TYPE_ACTION) { + if (gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), &parent_iter, &iter)) { + gtk_tree_store_insert_after (self->priv->model, &new_iter, &parent_iter, &iter); + } else { + gtk_tree_store_insert_after (self->priv->model, &new_iter, NULL, &iter); + } + } else { + gtk_tree_store_insert_after (self->priv->model, &new_iter, &iter, NULL); + } + + data = row_data_new (); + data->uuid = g_strdup (default_label); + data->type = type; + if (type == ROW_TYPE_SUBMENU) { + data->user_label = g_strdup (default_label); + } + + g_autofree gchar *hash = g_uuid_string_random (); + gtk_tree_store_set (self->priv->model, &new_iter, + COL_HASH, hash, + COL_UUID, data->uuid, + COL_TYPE, type, + COL_ROW_DATA, data, + -1); + + row_data_free (data); + gtk_tree_selection_select_iter (gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->treeview)), + &new_iter); + + if (grab_name_focus) { + gtk_widget_grab_focus (self->priv->name_entry); + } + + gtk_tree_path_free (path); + selected_row_changed (self, TRUE); +} + +static void +on_new_submenu_clicked (GtkMenuItem *item, NemoActionLayoutEditor *self) +{ + insert_new_row_at_selection (self, ROW_TYPE_SUBMENU, _("New submenu"), TRUE); +} + +static void +on_new_separator_clicked (GtkMenuItem *item, NemoActionLayoutEditor *self) +{ + insert_new_row_at_selection (self, ROW_TYPE_SEPARATOR, "separator", FALSE); +} + +static void +on_clear_icon_clicked (GtkMenuItem *item, NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + + RowData *data = get_selected_row_data (self, &iter); + if (data) { + g_free (data->user_icon); + data->user_icon = g_strdup (""); + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); + } +} + +static void +on_original_icon_clicked (GtkMenuItem *item, NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + + RowData *data = get_selected_row_data (self, &iter); + if (data) { + g_free (data->user_icon); + data->user_icon = NULL; + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); + } +} + +static void +on_choose_icon_clicked (GtkMenuItem *item, NemoActionLayoutEditor *self) +{ + XAppIconChooserDialog *chooser; + GtkTreeIter iter; + RowData *data; + gchar *icon_string; + gint response; + + data = get_selected_row_data (self, &iter); + if (!data) + return; + + chooser = xapp_icon_chooser_dialog_new (); + + icon_string = row_data_get_icon_string (data, FALSE); + if (icon_string != NULL && icon_string[0] != '\0') { + response = xapp_icon_chooser_dialog_run_with_icon (chooser, icon_string); + } else { + response = xapp_icon_chooser_dialog_run (chooser); + } + g_free (icon_string); + + if (response == GTK_RESPONSE_OK) { + g_free (data->user_icon); + data->user_icon = xapp_icon_chooser_dialog_get_icon_string (chooser); + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); + } + + gtk_widget_hide (GTK_WIDGET (chooser)); + gtk_widget_destroy (GTK_WIDGET (chooser)); +} + +static void +on_remove_submenu_clicked (GtkButton *button, NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + RowType row_type; + + if (!get_selected_row (self, NULL, &iter)) + return; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_TYPE, &row_type, + -1); + + if (row_type == ROW_TYPE_ACTION) { + return; + } + + /* For submenus, move children to parent level before removing */ + if (row_type == ROW_TYPE_SUBMENU) { + GtkTreeIter parent_iter, child_iter; + gboolean has_parent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), + &parent_iter, &iter); + + /* Move all children up one level */ + while (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (self->priv->model), &iter) > 0) { + GtkTreeIter new_iter; + RowData *child_data; + gchar *child_uuid, *child_type, *child_hash; + + gtk_tree_model_iter_children (GTK_TREE_MODEL (self->priv->model), &child_iter, &iter); + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &child_iter, + COL_HASH, &child_hash, + COL_UUID, &child_uuid, + COL_TYPE, &child_type, + COL_ROW_DATA, &child_data, + -1); + + if (has_parent) { + gtk_tree_store_insert_before (self->priv->model, &new_iter, &parent_iter, &iter); + } else { + gtk_tree_store_insert_before (self->priv->model, &new_iter, NULL, &iter); + } + + gtk_tree_store_set (self->priv->model, &new_iter, + COL_HASH, child_hash, + COL_UUID, child_uuid, + COL_TYPE, child_type, + COL_ROW_DATA, child_data, + -1); + + row_data_free (child_data); + + gtk_tree_store_remove (self->priv->model, &child_iter); + + g_free (child_hash); + g_free (child_uuid); + } + } + + gtk_tree_store_remove (self->priv->model, &iter); + + selected_row_changed (self, TRUE); +} + +// DND + +static void +dnd_autoscroll_cancel (NemoActionLayoutEditor *self) +{ + if (self->priv->dnd_autoscroll_timeout_id > 0) { + g_source_remove (self->priv->dnd_autoscroll_timeout_id); + self->priv->dnd_autoscroll_timeout_id = 0; + } +} + +static gboolean +dnd_autoscroll_timeout (gpointer user_data) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (user_data); + self->priv->dnd_autoscroll_timeout_id = 0; + return G_SOURCE_REMOVE; +} + +static void +dnd_autoscroll_start (NemoActionLayoutEditor *self) +{ + if (self->priv->dnd_autoscroll_timeout_id > 0) + g_source_remove (self->priv->dnd_autoscroll_timeout_id); + self->priv->dnd_autoscroll_timeout_id = g_timeout_add (50, dnd_autoscroll_timeout, self); +} + +static void +gather_row_surfaces (NemoActionLayoutEditor *self, + GtkTreeIter *iter, + GList **surfaces, + gint *width, + gint *height) +{ + GtkTreeIter child_iter; + RowType row_type; + gint child_count = 0; + + if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (self->priv->model), &child_iter, iter)) { + return; + } + + do { + GtkTreePath *child_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &child_iter); + + cairo_surface_t *surface = gtk_tree_view_create_row_drag_icon (GTK_TREE_VIEW (self->priv->treeview), + child_path); + + cairo_t *cr_measure = cairo_create (surface); + double x1, y1, x2, y2; + cairo_clip_extents (cr_measure, &x1, &y1, &x2, &y2); + cairo_destroy (cr_measure); + + gint surf_width = (gint)(x2 - x1); + gint surf_height = (gint)(y2 - y1); + + *surfaces = g_list_prepend (*surfaces, surface); + child_count++; + + *width = MAX (*width, surf_width); + *height += surf_height - 1; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &child_iter, + COL_TYPE, &row_type, -1); + + if (row_type == ROW_TYPE_SUBMENU) { + gather_row_surfaces (self, &child_iter, surfaces, width, height); + } + + gtk_tree_path_free (child_path); + } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &child_iter)); +} + +static void +on_drag_begin (GtkWidget *widget, GdkDragContext *context, NemoActionLayoutEditor *self) +{ + GtkTreePath *path; + GtkTreeIter iter; + cairo_surface_t *source_surface, *final_surface; + GList *surfaces = NULL; + gint width, height; + gint scale; + cairo_t *cr; + gdouble y; + gboolean first; + + if (!get_selected_row (self, &path, &iter)) + return; + + source_surface = gtk_tree_view_create_row_drag_icon (GTK_TREE_VIEW (self->priv->treeview), path); + cairo_t *cr_measure = cairo_create (source_surface); + double x1, y1, x2, y2; + cairo_clip_extents (cr_measure, &x1, &y1, &x2, &y2); + cairo_destroy (cr_measure); + + width = (gint)(x2 - x1); + height = (gint)(y2 - y1) - 1; + + surfaces = g_list_prepend (surfaces, source_surface); + gather_row_surfaces (self, &iter, &surfaces, &width, &height); + surfaces = g_list_reverse (surfaces); + + scale = gtk_widget_get_scale_factor (widget); + final_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width + 4, height + 4); + cairo_surface_set_device_scale (final_surface, scale, scale); + + cr = cairo_create (final_surface); + + y = 2.0; + first = TRUE; + + for (GList *l = surfaces; l != NULL; l = l->next) { + cairo_surface_t *s = (cairo_surface_t *) l->data; + + cairo_t *cr_measure = cairo_create (s); + double sx1, sy1, sx2, sy2; + cairo_clip_extents (cr_measure, &sx1, &sy1, &sx2, &sy2); + cairo_destroy (cr_measure); + gint surf_height = (gint)(sy2 - sy1); + + cairo_save (cr); + cairo_set_source_surface (cr, s, 2.0, y); + cairo_paint (cr); + cairo_restore (cr); + + if (!first) { + cairo_save (cr); + cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); + cairo_rectangle (cr, 1.0 * scale, (y - 2.0) * scale, + (width / (gdouble)scale) - 2.0, 4.0); + cairo_fill (cr); + cairo_restore (cr); + } + first = FALSE; + + y += surf_height / (gdouble)scale - 1.0; + cairo_surface_destroy (s); + } + + cairo_show_page (cr); + cairo_destroy (cr); + + gtk_drag_set_icon_surface (context, final_surface); + cairo_surface_destroy (final_surface); + + g_list_free (surfaces); + gtk_tree_path_free (path); +} + +static void +on_drag_end (GtkWidget *widget, GdkDragContext *context, NemoActionLayoutEditor *self) +{ + dnd_autoscroll_cancel (self); +} + +static gboolean +on_drag_motion (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + guint time, + NemoActionLayoutEditor *self) +{ + GtkTreePath *path = NULL; + GtkTreeViewDropPosition pos; + GtkTreeIter iter, source_iter; + GtkTreePath *source_path; + RowType target_type; + + if (!gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (widget), x, y, &path, &pos)) { + gdk_drag_status (context, 0, time); + return FALSE; + } + + if (!get_selected_row (self, &source_path, &source_iter)) { + gtk_tree_path_free (path); + return FALSE; + } + + // Don't allow dropping on itself + if (gtk_tree_path_compare (source_path, path) == 0) { + gtk_tree_path_free (path); + gtk_tree_path_free (source_path); + gdk_drag_status (context, 0, time); + return FALSE; + } + + // Don't allow dropping into own hierarchy + if (gtk_tree_path_is_ancestor (source_path, path) || + gtk_tree_path_is_descendant (source_path, path)) { + gtk_tree_path_free (path); + gtk_tree_path_free (source_path); + gdk_drag_status (context, 0, time); + return FALSE; + } + + gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), &iter, path); + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_TYPE, &target_type, -1); + + // Don't allow INTO on actions/separators - only on submenus + if (target_type == ROW_TYPE_ACTION || + target_type == ROW_TYPE_SEPARATOR) { + if (pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE || + pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER) { + gtk_tree_path_free (path); + gtk_tree_path_free (source_path); + gdk_drag_status (context, 0, time); + return FALSE; + } + } + + gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), path, pos); + gdk_drag_status (context, GDK_ACTION_MOVE, time); + + dnd_autoscroll_start (self); + + gtk_tree_path_free (path); + gtk_tree_path_free (source_path); + return TRUE; +} + +static void +on_drag_data_get (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time, + NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + gchar *hash, *uuid; + RowType type; + gchar *data_str; + + if (!get_selected_row (self, NULL, &iter)) + return; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_HASH, &hash, + COL_UUID, &uuid, + COL_TYPE, &type, + -1); + + data_str = g_strdup_printf ("%s::%s::%s", hash, uuid, row_type_to_string (type)); + + gtk_selection_data_set_text (selection_data, data_str, -1); + + g_free (data_str); + g_free (hash); + g_free (uuid); +} + +static void +on_drag_data_received (GtkWidget *widget, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time, + NemoActionLayoutEditor *self) +{ + GtkTreePath *path = NULL; + GtkTreeViewDropPosition pos; + GtkTreeIter target_iter, source_iter, new_iter, parent_iter; + const guchar *data; + gchar **parts; + const gchar *source_hash, *source_uuid; + RowType source_type, target_type; + RowData *row_data; + gchar *new_hash; + gboolean has_parent; + + if (!gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (widget), x, y, &path, &pos)) { + gtk_drag_finish (context, FALSE, FALSE, time); + return; + } + + data = gtk_selection_data_get_text (selection_data); + if (!data) { + gtk_tree_path_free (path); + gtk_drag_finish (context, FALSE, FALSE, time); + return; + } + + parts = g_strsplit ((const gchar *) data, "::", 3); + source_hash = parts[0]; + source_uuid = parts[1]; + source_type = row_type_from_string (parts[2]); + g_free ((gchar *) data); + + if (!lookup_iter_by_hash (GTK_TREE_MODEL (self->priv->model), source_hash, &source_iter)) { + g_warning ("Source row not found"); + g_strfreev (parts); + gtk_tree_path_free (path); + gtk_drag_finish (context, FALSE, FALSE, time); + return; + } + + GtkTreePath *source_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &source_iter); + if (gtk_tree_path_compare (source_path, path) == 0 || + gtk_tree_path_is_ancestor (source_path, path) || + gtk_tree_path_is_descendant (source_path, path)) { + gtk_tree_path_free (source_path); + g_strfreev (parts); + gtk_tree_path_free (path); + gtk_drag_finish (context, FALSE, FALSE, time); + return; + } + gtk_tree_path_free (source_path); + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &source_iter, + COL_ROW_DATA, &row_data, -1); + + gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), &target_iter, path); + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &target_iter, + COL_TYPE, &target_type, -1); + + has_parent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), &parent_iter, &target_iter); + new_hash = g_uuid_string_random (); + + if (target_type == ROW_TYPE_SUBMENU && + (pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE || + pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER || + pos == GTK_TREE_VIEW_DROP_AFTER)) { + gtk_tree_store_insert (self->priv->model, &new_iter, &target_iter, 0); + } else { + if (pos == GTK_TREE_VIEW_DROP_BEFORE) { + gtk_tree_store_insert_before (self->priv->model, &new_iter, + has_parent ? &parent_iter : NULL, + &target_iter); + } else { + gtk_tree_store_insert_after (self->priv->model, &new_iter, + has_parent ? &parent_iter : NULL, + &target_iter); + } + } + + gtk_tree_store_set (self->priv->model, &new_iter, + COL_HASH, new_hash, + COL_UUID, source_uuid, + COL_TYPE, source_type, + COL_ROW_DATA, row_data, + -1); + + row_data_free (row_data); + + if (source_type == ROW_TYPE_SUBMENU) { + move_tree_recursive (self, &source_iter, &new_iter); + } + + remove_row_by_hash (self, source_hash); + select_row_by_hash (self, new_hash); + + g_free (new_hash); + g_strfreev (parts); + gtk_tree_path_free (path); + + gtk_drag_finish (context, TRUE, TRUE, time); + set_needs_saved (self, TRUE); +} + +static void +on_name_entry_changed (GtkEntry *entry, NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + RowData *data; + const gchar *text; + + if (self->priv->updating_row_edit_fields) + return; + + data = get_selected_row_data (self, &iter); + if (data == NULL) + return; + + text = gtk_entry_get_text (entry); + g_free (data->user_label); + data->user_label = g_strdup (text); + + /* For submenus, uuid matches the label */ + if (data->type == ROW_TYPE_SUBMENU) { + g_free (data->uuid); + data->uuid = g_strdup (text); + gtk_tree_store_set (self->priv->model, &iter, + COL_UUID, text, + -1); + } + + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); +} + +static void +on_name_entry_icon_press (GtkEntry *entry, + GtkEntryIconPosition icon_pos, + GdkEvent *event, + NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + RowData *data; + + if (icon_pos != GTK_ENTRY_ICON_SECONDARY) + return; + + data = get_selected_row_data (self, &iter); + if (data == NULL) + return; + + g_clear_pointer (&data->user_label, g_free); + + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); +} + +// Shortcuts + +static void +accel_editing_done (GtkCellEditable *editable, + gpointer user_data) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (user_data); + self->priv->editing_accel = FALSE; + g_signal_handlers_disconnect_by_data (editable, user_data); +} + +static void +on_accel_edit_started (GtkCellRenderer *renderer, + GtkCellEditable *editable, + char *path, + gpointer user_data) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (user_data); + self->priv->editing_accel = TRUE; + g_signal_connect (editable, "editing-done", G_CALLBACK (accel_editing_done), self); +} + +static gboolean +find_conflict_recursively (NemoActionLayoutEditor *self, + guint key, + GdkModifierType mods, + const gchar *selected_hash, + GtkTreeIter *parent_iter) +{ + GtkTreeIter child_iter; + gboolean valid; + + if (parent_iter) { + valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (self->priv->model), + &child_iter, parent_iter); + } else { + valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->model), + &child_iter); + } + + // Return TRUE if a conflict was found and the user wanted to preserve the + // existing keybinding. FALSE if they wish to overwrite (or no conflict was + // found). + + while (valid) { + RowData *data; + RowType row_type; + g_autofree gchar *hash = NULL; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &child_iter, + COL_HASH, &hash, + -1); + + if (g_strcmp0 (hash, selected_hash) == 0) { + valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &child_iter); + continue; + } + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &child_iter, + COL_ROW_DATA, &data, + COL_TYPE, &row_type, + -1); + + if (data && data->accelerator && data->accelerator[0] != '\0') { + guint row_key = 0; + GdkModifierType row_mods = 0; + gtk_accelerator_parse (data->accelerator, &row_key, &row_mods); + + if (row_key == key && row_mods == mods) { + gchar *label = row_data_get_label (data); + // FIXME: bad string for translation + gchar *message = g_strdup_printf ( + _("This key combination is already in use by another action:\n\n%s\n\nDo you want to replace it?"), + label); + + GtkWidget *dialog = gtk_message_dialog_new ( + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))), + GTK_DIALOG_MODAL, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_YES_NO, + NULL); + gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), message); + + gint response = gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + g_free (label); + g_free (message); + + if (response == GTK_RESPONSE_YES) { + g_clear_pointer (&data->accelerator, g_free); + tree_store_update_row_data (self->priv->model, &child_iter, data); + return FALSE; + } else { + row_data_free (data); + return TRUE; + } + } + } + + if (data != NULL) { + row_data_free (data); + } + + if (row_type == ROW_TYPE_SUBMENU) { + if (find_conflict_recursively (self, key, mods, selected_hash, &child_iter)) { + return TRUE; + } + } + + valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &child_iter); + } + + return FALSE; +} + +static gboolean +overwrite_any_existing (NemoActionLayoutEditor *self, + guint key, + GdkModifierType mods, + const gchar *selected_hash) +{ + GList *l; + + // Check against built-in Nemo shortcuts, fail if there's conflict. + for (l = self->priv->builtin_shortcuts; l != NULL; l = l->next) { + BuiltinShortcut *shortcut = l->data; + if (shortcut->key == key && shortcut->mods == mods) { + GtkWidget *parent_window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW); + gchar *message = g_markup_printf_escaped ( + _("This key combination is already in use by Nemo (%s). It cannot be changed."), + shortcut->label); + + GtkWidget *dialog = gtk_message_dialog_new ( + GTK_WINDOW (parent_window), + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + NULL); + gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), message); + + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + g_free (message); + return FALSE; + } + } + + // Then check for conflicts with other actions + return !find_conflict_recursively (self, key, mods, selected_hash, NULL); +} + +static void +on_accel_edited (GtkCellRendererAccel *cell, + const gchar *path_string, + guint key, + GdkModifierType mods, + guint hardware_keycode, + gpointer user_data) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (user_data); + GtkTreeIter iter; + RowData *data; + g_autofree gchar *new_accel = NULL; + g_autofree gchar *selected_hash = NULL; + + data = get_selected_row_data (self, &iter); + if (data == NULL) + return; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, COL_HASH, &selected_hash, -1); + + if (!overwrite_any_existing (self, key, mods, selected_hash)) { + row_data_free (data); + return; + } + + new_accel = gtk_accelerator_name (key, mods); + if (g_strcmp0 (data->accelerator, new_accel) == 0) { + row_data_free (data); + return; + } + + g_free (data->accelerator); + data->accelerator = g_strdup (new_accel); + + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); +} + +static void +on_accel_cleared (GtkCellRendererAccel *cell, + const gchar *path_string, + gpointer user_data) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (user_data); + GtkTreeIter iter; + RowData *data; + + data = get_selected_row_data (self, &iter); + if (data == NULL) + return; + + g_free (data->accelerator); + data->accelerator = NULL; + + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, TRUE); +} + +static GtkTreeIter +get_last_at_level (GtkTreeModel *model, GtkTreeIter *iter) +{ + if (gtk_tree_model_iter_has_child (model, iter)) { + GtkTreeIter child, last; + gtk_tree_model_iter_children (model, &child, iter); + + while (TRUE) { + last = child; + if (!gtk_tree_model_iter_next (model, &child)) + break; + } + + return get_last_at_level (model, &last); + } + return *iter; +} + +static gboolean +same_iter (GtkTreeModel *model, GtkTreeIter *iter1, GtkTreeIter *iter2) +{ + if (iter1 == NULL && iter2 == NULL) + return TRUE; + if (iter1 == NULL || iter2 == NULL) + return FALSE; + + GtkTreePath *path1 = gtk_tree_model_get_path (model, iter1); + GtkTreePath *path2 = gtk_tree_model_get_path (model, iter2); + gint result = gtk_tree_path_compare (path1, path2); + gtk_tree_path_free (path1); + gtk_tree_path_free (path2); + + return result == 0; +} + +static gboolean +path_is_valid (GtkTreeModel *model, GtkTreePath *path) +{ + GtkTreeIter iter; + return gtk_tree_model_get_iter (model, &iter, path); +} + +static gboolean +next_path_validated (GtkTreeModel *model, GtkTreePath *path) +{ + gtk_tree_path_next (path); + return path_is_valid (model, path); +} + +typedef struct { + const gchar *hash; + GtkTreeIter *result; + gboolean found; +} LookupData; + +static gboolean +lookup_iter_foreach (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + LookupData *data = user_data; + g_autofree gchar *current_hash = NULL; + + gtk_tree_model_get (model, iter, COL_HASH, ¤t_hash, -1); + + if (g_strcmp0 (current_hash, data->hash) == 0) { + *data->result = *iter; + data->found = TRUE; + return TRUE; + } + + return FALSE; +} + +static gboolean +lookup_iter_by_hash (GtkTreeModel *model, const gchar *hash, GtkTreeIter *result) +{ + LookupData data = { hash, result, FALSE }; + gtk_tree_model_foreach (model, lookup_iter_foreach, &data); + return data.found; +} + +static void +remove_row_by_hash (NemoActionLayoutEditor *self, const gchar *hash) +{ + GtkTreeIter iter; + if (lookup_iter_by_hash (GTK_TREE_MODEL (self->priv->model), hash, &iter)) { + gtk_tree_store_remove (self->priv->model, &iter); + } +} + +static void +select_row_by_hash (NemoActionLayoutEditor *self, const gchar *hash) +{ + GtkTreeIter iter; + if (lookup_iter_by_hash (GTK_TREE_MODEL (self->priv->model), hash, &iter)) { + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->treeview)); + gtk_tree_selection_select_iter (selection, &iter); + } +} + +static void +move_tree_recursive (NemoActionLayoutEditor *self, GtkTreeIter *source_iter, GtkTreeIter *new_parent) +{ + GtkTreeIter child; + if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (self->priv->model), &child, source_iter)) + return; + + do { + gchar *hash, *uuid; + RowType type; + RowData *data; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &child, + COL_HASH, &hash, + COL_UUID, &uuid, + COL_TYPE, &type, + COL_ROW_DATA, &data, + -1); + + GtkTreeIter new_child; + gchar *new_hash = g_uuid_string_random (); + gtk_tree_store_insert (self->priv->model, &new_child, new_parent, -1); + gtk_tree_store_set (self->priv->model, &new_child, + COL_HASH, new_hash, + COL_UUID, uuid, + COL_TYPE, type, + COL_ROW_DATA, data, + -1); + + row_data_free (data); + + if (type == ROW_TYPE_SUBMENU) { + move_tree_recursive (self, &child, &new_child); + } + + g_free (new_hash); + g_free (hash); + g_free (uuid); + } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &child)); +} + +static void +on_up_button_clicked (GtkButton *button, NemoActionLayoutEditor *self) +{ + GtkTreePath *path; + GtkTreeIter iter; + + if (!get_selected_row (self, &path, &iter)) + return; + + GtkTreeIter parent; + gboolean has_parent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), &parent, &iter); + GtkTreeIter *parent_ptr = has_parent ? &parent : NULL; + + gchar *source_hash, *source_uuid; + RowType source_type; + RowData *source_data; + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_HASH, &source_hash, + COL_UUID, &source_uuid, + COL_TYPE, &source_type, + COL_ROW_DATA, &source_data, + -1); + + gchar *target_hash = g_uuid_string_random (); + GtkTreeIter inserted_iter; + GtkTreeIter *target_parent = NULL; + gboolean inserted = FALSE; + gboolean source_was_expanded = FALSE; + + GtkTreePath *target_path = gtk_tree_path_copy (path); + + if (gtk_tree_path_prev (target_path)) { + /* Move before previous sibling or into it */ + GtkTreeIter target_iter; + gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), &target_iter, target_path); + target_iter = get_last_at_level (GTK_TREE_MODEL (self->priv->model), &target_iter); + + RowType target_iter_type; + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &target_iter, + COL_TYPE, &target_iter_type, -1); + + if (target_iter_type == ROW_TYPE_SUBMENU) { + gtk_tree_store_prepend (self->priv->model, &inserted_iter, &target_iter); + target_parent = gtk_tree_iter_copy (&target_iter); + } else { + GtkTreeIter target_iter_parent; + gboolean has_target_parent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), + &target_iter_parent, &target_iter); + + /* Check if both items are at the same level */ + gboolean same_level = FALSE; + if (has_target_parent && has_parent) { + same_level = same_iter (GTK_TREE_MODEL (self->priv->model), parent_ptr, &target_iter_parent); + } else if (!has_target_parent && !has_parent) { + /* Both at root level */ + same_level = TRUE; + } + + if (same_level) { + gtk_tree_store_insert_before (self->priv->model, &inserted_iter, + has_target_parent ? &target_iter_parent : NULL, + &target_iter); + if (has_target_parent) + target_parent = gtk_tree_iter_copy (&target_iter_parent); + } else { + gtk_tree_store_insert_after (self->priv->model, &inserted_iter, + has_target_parent ? &target_iter_parent : NULL, + &target_iter); + if (has_target_parent) + target_parent = gtk_tree_iter_copy (&target_iter_parent); + } + } + inserted = TRUE; + } else if (has_parent) { + /* We're the first child of our parent - insert before parent at grandparent level */ + GtkTreeIter grandparent; + gboolean has_grandparent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), + &grandparent, &parent); + gtk_tree_store_insert_before (self->priv->model, &inserted_iter, + has_grandparent ? &grandparent : NULL, + &parent); + if (has_grandparent) + target_parent = gtk_tree_iter_copy (&grandparent); + inserted = TRUE; + } else { + /* At root level and already first - nowhere to go */ + } + + if (inserted) { + self->priv->updating_model = TRUE; + + gtk_tree_store_set (self->priv->model, &inserted_iter, + COL_HASH, target_hash, + COL_UUID, source_uuid, + COL_TYPE, source_type, + COL_ROW_DATA, source_data, + -1); + + row_data_free (source_data); + + if (source_type == ROW_TYPE_SUBMENU) { + GtkTreePath *source_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &iter); + source_was_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (self->priv->treeview), source_path); + gtk_tree_path_free (source_path); + + move_tree_recursive (self, &iter, &inserted_iter); + } + + if (target_parent) { + GtkTreePath *parent_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), target_parent); + gtk_tree_view_expand_row (GTK_TREE_VIEW (self->priv->treeview), parent_path, TRUE); + gtk_tree_path_free (parent_path); + gtk_tree_iter_free (target_parent); + } else if (source_was_expanded) { + GtkTreePath *inserted_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &inserted_iter); + gtk_tree_view_expand_row (GTK_TREE_VIEW (self->priv->treeview), inserted_path, TRUE); + gtk_tree_path_free (inserted_path); + } + + remove_row_by_hash (self, source_hash); + self->priv->updating_model = FALSE; + + select_row_by_hash (self, target_hash); + + GtkTreePath *scroll_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &inserted_iter); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (self->priv->treeview), scroll_path, NULL, FALSE, 0, 0); + gtk_tree_path_free (scroll_path); + + set_needs_saved (self, TRUE); + } + + g_free (target_hash); + g_free (source_hash); + g_free (source_uuid); + gtk_tree_path_free (target_path); + gtk_tree_path_free (path); +} + +static void +on_down_button_clicked (GtkButton *button, NemoActionLayoutEditor *self) +{ + GtkTreePath *path; + GtkTreeIter iter; + + if (!get_selected_row (self, &path, &iter)) + return; + + gchar *source_hash, *source_uuid; + RowType source_type; + RowData *source_data; + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_HASH, &source_hash, + COL_UUID, &source_uuid, + COL_TYPE, &source_type, + COL_ROW_DATA, &source_data, + -1); + + gchar *target_hash = g_uuid_string_random (); + GtkTreeIter inserted_iter; + GtkTreeIter *target_parent = NULL; + gboolean inserted = FALSE; + gboolean source_was_expanded = FALSE; + + GtkTreePath *target_path = gtk_tree_path_copy (path); + + if (next_path_validated (GTK_TREE_MODEL (self->priv->model), target_path)) { + /* Maybe move into submenu or after next sibling */ + GtkTreeIter maybe_submenu; + gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), &maybe_submenu, target_path); + + RowType maybe_submenu_type; + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &maybe_submenu, + COL_TYPE, &maybe_submenu_type, -1); + + if (maybe_submenu_type == ROW_TYPE_SUBMENU) { + gtk_tree_store_prepend (self->priv->model, &inserted_iter, &maybe_submenu); + target_parent = gtk_tree_iter_copy (&maybe_submenu); + } else { + GtkTreeIter target_iter_parent; + gboolean has_parent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), + &target_iter_parent, &maybe_submenu); + gtk_tree_store_insert_after (self->priv->model, &inserted_iter, + has_parent ? &target_iter_parent : NULL, + &maybe_submenu); + if (has_parent) + target_parent = gtk_tree_iter_copy (&target_iter_parent); + } + inserted = TRUE; + } else { + /* No next sibling - try to move up and continue after parent */ + gtk_tree_path_free (target_path); + target_path = gtk_tree_path_copy (path); + + /* Keep trying to go up until we find a valid next position */ + while (gtk_tree_path_get_depth (target_path) > 1) { + if (!gtk_tree_path_up (target_path)) + break; + + if (!path_is_valid (GTK_TREE_MODEL (self->priv->model), target_path)) + break; + + /* Now try to go to the next sibling of this parent */ + GtkTreePath *test_path = gtk_tree_path_copy (target_path); + if (next_path_validated (GTK_TREE_MODEL (self->priv->model), test_path)) { + /* Found a next position - use it */ + GtkTreeIter target_iter; + gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), &target_iter, test_path); + + GtkTreeIter target_iter_parent; + gboolean has_parent = gtk_tree_model_iter_parent (GTK_TREE_MODEL (self->priv->model), + &target_iter_parent, &target_iter); + + /* Check if it's a submenu */ + RowType test_type; + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &target_iter, + COL_TYPE, &test_type, -1); + + if (test_type == ROW_TYPE_SUBMENU) { + gtk_tree_store_prepend (self->priv->model, &inserted_iter, &target_iter); + target_parent = gtk_tree_iter_copy (&target_iter); + } else { + gtk_tree_store_insert_after (self->priv->model, &inserted_iter, + has_parent ? &target_iter_parent : NULL, + &target_iter); + if (has_parent) + target_parent = gtk_tree_iter_copy (&target_iter_parent); + } + gtk_tree_path_free (test_path); + inserted = TRUE; + break; + } else { + /* No next sibling at this level, try going up another level */ + gtk_tree_path_free (test_path); + continue; + } + } + } + + if (inserted) { + self->priv->updating_model = TRUE; + + gtk_tree_store_set (self->priv->model, &inserted_iter, + COL_HASH, target_hash, + COL_UUID, source_uuid, + COL_TYPE, source_type, + COL_ROW_DATA, source_data, + -1); + row_data_free (source_data); + + if (source_type == ROW_TYPE_SUBMENU) { + GtkTreePath *source_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &iter); + source_was_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (self->priv->treeview), source_path); + gtk_tree_path_free (source_path); + + move_tree_recursive (self, &iter, &inserted_iter); + } + + if (target_parent) { + GtkTreePath *parent_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), target_parent); + gtk_tree_view_expand_row (GTK_TREE_VIEW (self->priv->treeview), parent_path, TRUE); + gtk_tree_path_free (parent_path); + gtk_tree_iter_free (target_parent); + } else if (source_was_expanded) { + GtkTreePath *inserted_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &inserted_iter); + gtk_tree_view_expand_row (GTK_TREE_VIEW (self->priv->treeview), inserted_path, TRUE); + gtk_tree_path_free (inserted_path); + } + + remove_row_by_hash (self, source_hash); + self->priv->updating_model = FALSE; + + select_row_by_hash (self, target_hash); + + GtkTreePath *scroll_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &inserted_iter); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (self->priv->treeview), scroll_path, NULL, FALSE, 0, 0); + gtk_tree_path_free (scroll_path); + + set_needs_saved (self, TRUE); + } + + g_free (target_hash); + g_free (source_hash); + g_free (source_uuid); + gtk_tree_path_free (target_path); + gtk_tree_path_free (path); +} + +static void +on_row_activated (GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, + NemoActionLayoutEditor *self) +{ + GtkTreeIter iter; + + RowData *data = get_selected_row_data (self, &iter); + + if (data && data->type == ROW_TYPE_ACTION) { + data->enabled = !data->enabled; + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, FALSE); + save_disabled_list (self); + } +} + +static void +on_action_row_toggled (GtkCellRendererToggle *cell, gchar *path_str, NemoActionLayoutEditor *self) +{ + GtkTreePath *path = gtk_tree_path_new_from_string (path_str); + GtkTreeIter iter; + RowData *data; + + if (gtk_tree_model_get_iter (GTK_TREE_MODEL (self->priv->model), &iter, path)) { + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_ROW_DATA, &data, + -1); + + if (data) { + data->enabled = !data->enabled; + tree_store_update_row_data (self->priv->model, &iter, data); + selected_row_changed (self, FALSE); + save_disabled_list (self); + } + } + + gtk_tree_path_free (path); +} + +// CellRenderer functions + +static void +toggle_render_func (GtkTreeViewColumn *column, GtkCellRenderer *cell, + GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) +{ + RowType row_type; + RowData *data; + + gtk_tree_model_get (model, iter, + COL_TYPE, &row_type, + COL_ROW_DATA, &data, + -1); + + if (row_type == ROW_TYPE_SUBMENU || + row_type == ROW_TYPE_SEPARATOR) { + g_object_set (cell, "visible", FALSE, NULL); + } else { + g_object_set (cell, + "visible", TRUE, + "active", data ? data->enabled : FALSE, + NULL); + } + + if (data != NULL) { + row_data_free (data); + } + +} + +static void +menu_icon_render_func (GtkTreeViewColumn *column, GtkCellRenderer *cell, + GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) +{ + RowData *data; + gchar *icon_string; + + gtk_tree_model_get (model, iter, COL_ROW_DATA, &data, -1); + + if (data) { + icon_string = row_data_get_icon_string (data, FALSE); + if (icon_string) { + g_object_set (cell, "icon-name", icon_string, NULL); + g_free (icon_string); + } else { + g_object_set (cell, "icon-name", NULL, NULL); + } + + row_data_free (data); + } +} + +static void +menu_label_render_func (GtkTreeViewColumn *column, GtkCellRenderer *cell, + GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) +{ + RowType row_type; + RowData *data; + gchar *label; + + gtk_tree_model_get (model, iter, + COL_TYPE, &row_type, + COL_ROW_DATA, &data, + -1); + + if (data) { + label = row_data_get_label (data); + + if (row_type == ROW_TYPE_SUBMENU) { + gchar *markup = g_strdup_printf ("%s", label); + g_object_set (cell, + "markup", markup, + "weight", PANGO_WEIGHT_BOLD, + "style", PANGO_STYLE_NORMAL, + NULL); + g_free (markup); + } else { + g_object_set (cell, + "text", label, + "weight", data->enabled ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_ULTRALIGHT, + "style", data->enabled ? PANGO_STYLE_NORMAL : PANGO_STYLE_ITALIC, + NULL); + } + + g_free (label); + row_data_free (data); + } + +} + +static void +accel_render_func (GtkTreeViewColumn *column, GtkCellRenderer *cell, + GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (user_data); + RowType row_type; + RowData *data; + guint key = 0; + GdkModifierType mods = 0; + + gtk_tree_model_get (model, iter, + COL_TYPE, &row_type, + COL_ROW_DATA, &data, + -1); + + if (row_type == ROW_TYPE_SUBMENU || + row_type == ROW_TYPE_SEPARATOR) { + g_object_set (cell, "visible", FALSE, NULL); + + if (data != NULL) { + row_data_free (data); + } + return; + } + + if (data && data->accelerator) { + gtk_accelerator_parse (data->accelerator, &key, &mods); + } + + g_object_set (cell, + "visible", TRUE, + "accel-key", key, + "accel-mods", mods, + NULL); + + /* Set text only for empty accelerators */ + if (!data || !data->accelerator || data->accelerator[0] == '\0') { + GtkTreeIter selected_iter; + gboolean is_selected = FALSE; + + if (get_selected_row (self, NULL, &selected_iter)) { + GtkTreePath *current_path = gtk_tree_model_get_path (model, iter); + GtkTreePath *selected_path = gtk_tree_model_get_path (model, &selected_iter); + + if (current_path && selected_path) { + is_selected = (gtk_tree_path_compare (current_path, selected_path) == 0); + } + + if (current_path) + gtk_tree_path_free (current_path); + if (selected_path) + gtk_tree_path_free (selected_path); + } + + if (is_selected) { + if (!self->priv->editing_accel) { + g_object_set (cell, "text", _("Click to add a shortcut"), NULL); + } else { + g_object_set (cell, "text", NULL, NULL); + } + } else { + g_object_set (cell, "text", " ", NULL); + } + } + + if (data != NULL) { + row_data_free (data); + } +} + +static void +extract_shortcuts_recursive (NemoActionLayoutEditor *self, + GtkWidget *widget) +{ + if (GTK_IS_SHORTCUTS_SHORTCUT (widget)) { + g_autofree gchar *title = NULL; + g_autofree gchar *accelerator = NULL; + + /* Get the title and accelerator properties */ + g_object_get (widget, + "title", &title, + "accelerator", &accelerator, + NULL); + + self->priv->builtin_shortcuts = g_list_prepend (self->priv->builtin_shortcuts, + builtin_shortcut_new (title, accelerator)); + } + + if (GTK_IS_CONTAINER (widget)) { + GList *children = gtk_container_get_children (GTK_CONTAINER (widget)); + GList *l; + + for (l = children; l != NULL; l = l->next) { + extract_shortcuts_recursive (self, GTK_WIDGET (l->data)); + } + g_list_free (children); + } +} + +static void +load_builtin_shortcuts (NemoActionLayoutEditor *self) +{ + GtkBuilder *builder; + GError *error = NULL; + GObject *shortcuts_window; + + builder = gtk_builder_new (); + if (!gtk_builder_add_from_resource (builder, "/org/nemo/nemo-shortcuts.ui", &error)) { + g_warning ("Could not load nemo-shortcuts.ui from resource file - " + "we won't be able to detect built-in shortcut collisions: %s", + error->message); + g_error_free (error); + g_object_unref (builder); + return; + } + + shortcuts_window = gtk_builder_get_object (builder, "keyboard_shortcuts"); + if (!shortcuts_window) { + g_warning ("Could not find keyboard_shortcuts object in UI file"); + g_object_unref (builder); + return; + } + + extract_shortcuts_recursive (self, GTK_WIDGET (shortcuts_window)); + + g_object_unref (builder); +} + +static JsonArray * +serialize_model_recursive (NemoActionLayoutEditor *self, GtkTreeIter *parent) +{ + JsonArray *array = json_array_new (); + GtkTreeIter iter; + gboolean valid; + + if (parent) { + valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (self->priv->model), &iter, parent); + } else { + valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->model), &iter); + } + + while (valid) { + RowType row_type; + RowData *data; + JsonObject *obj; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_TYPE, &row_type, + COL_ROW_DATA, &data, + -1); + + if (data) { + obj = json_object_new (); + json_object_set_string_member (obj, "uuid", data->uuid); + json_object_set_string_member (obj, "type", row_type_to_string (data->type)); + + if (data->user_label) + json_object_set_string_member (obj, "user-label", data->user_label); + else + json_object_set_null_member (obj, "user-label"); + + if (data->user_icon) + json_object_set_string_member (obj, "user-icon", data->user_icon); + else + json_object_set_null_member (obj, "user-icon"); + + if (data->accelerator) + json_object_set_string_member (obj, "accelerator", data->accelerator); + else + json_object_set_null_member (obj, "accelerator"); + + if (row_type == ROW_TYPE_SUBMENU) { + JsonArray *children = serialize_model_recursive (self, &iter); + json_object_set_array_member (obj, "children", children); + } + + json_array_add_object_element (array, obj); + row_data_free (data); + } + + valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &iter); + } + + return array; +} + +static void +save_model (NemoActionLayoutEditor *self) +{ + gchar *config_dir = g_build_filename (g_get_user_config_dir (), "nemo", NULL); + gchar *json_file = g_build_filename (g_get_user_config_dir (), JSON_FILE, NULL); + JsonGenerator *generator; + JsonNode *root; + JsonObject *obj; + JsonArray *toplevel; + + g_mkdir_with_parents (config_dir, 0755); + + toplevel = serialize_model_recursive (self, NULL); + + obj = json_object_new (); + json_object_set_array_member (obj, "toplevel", toplevel); + + root = json_node_new (JSON_NODE_OBJECT); + json_node_set_object (root, obj); + + generator = json_generator_new (); + json_generator_set_root (generator, root); + json_generator_set_pretty (generator, TRUE); + + json_generator_to_file (generator, json_file, NULL); + + g_object_unref (generator); + json_node_free (root); + json_object_unref (obj); + g_free (json_file); + g_free (config_dir); +} + +static GHashTable * +load_installed_actions (NemoActionLayoutEditor *self) +{ + GHashTable *actions; + const gchar * const *data_dirs; + gint i; + + actions = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, + (GDestroyNotify) row_data_free); + + data_dirs = g_get_system_data_dirs (); + + for (i = 0; data_dirs[i] != NULL; i++) { + gchar *actions_dir = g_build_filename (data_dirs[i], USER_ACTIONS_DIR, NULL); + GDir *dir = g_dir_open (actions_dir, 0, NULL); + + if (dir) { + const gchar *name; + while ((name = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (name, ".nemo_action")) { + gchar *fullpath = g_build_filename (actions_dir, name, NULL); + GKeyFile *keyfile = g_key_file_new (); + + if (g_key_file_load_from_file (keyfile, fullpath, G_KEY_FILE_NONE, NULL)) { + RowData *data = row_data_new (); + data->uuid = g_strdup (name); + data->type = ROW_TYPE_ACTION; + data->filename = fullpath; + data->keyfile = keyfile; + + g_hash_table_replace (actions, g_strdup (name), data); + } else { + g_key_file_free (keyfile); + g_free (fullpath); + } + } + } + g_dir_close (dir); + } + g_free (actions_dir); + } + + gchar *user_actions_dir = g_build_filename (g_get_user_data_dir (), USER_ACTIONS_DIR, NULL); + GDir *dir = g_dir_open (user_actions_dir, 0, NULL); + + if (dir) { + const gchar *name; + while ((name = g_dir_read_name (dir)) != NULL) { + if (g_str_has_suffix (name, ".nemo_action")) { + gchar *fullpath = g_build_filename (user_actions_dir, name, NULL); + GKeyFile *keyfile = g_key_file_new (); + + if (g_key_file_load_from_file (keyfile, fullpath, G_KEY_FILE_NONE, NULL)) { + RowData *data = row_data_new (); + data->uuid = g_strdup (name); + data->type = ROW_TYPE_ACTION; + data->filename = fullpath; + data->keyfile = keyfile; + + /* User actions override system actions */ + g_hash_table_replace (actions, g_strdup (name), data); + } else { + g_key_file_free (keyfile); + g_free (fullpath); + } + } + } + g_dir_close (dir); + } + g_free (user_actions_dir); + + return actions; +} + +static gboolean +is_action_disabled_by_filename (const gchar *filename, gchar **disabled_actions) +{ + if (!disabled_actions || !filename) + return FALSE; + + gchar *basename = g_path_get_basename (filename); + gboolean is_disabled = FALSE; + + for (guint i = 0; disabled_actions[i] != NULL; i++) { + if (g_strcmp0 (disabled_actions[i], basename) == 0) { + is_disabled = TRUE; + break; + } + } + + g_free (basename); + return is_disabled; +} + +static void +fill_model_recursive (NemoActionLayoutEditor *self, JsonArray *array, + GtkTreeIter *parent, GHashTable *installed_actions, + gchar **disabled_actions) +{ + guint i, len; + + if (!array) + return; + + len = json_array_get_length (array); + + for (i = 0; i < len; i++) { + JsonObject *item = json_array_get_object_element (array, i); + const gchar *uuid = json_object_get_string_member (item, "uuid"); + const gchar *type_str = json_object_get_string_member (item, "type"); + RowType type = row_type_from_string (type_str); + GtkTreeIter iter; + RowData *data; + + if (type == ROW_TYPE_ACTION) { + gchar *key = NULL; + if (!g_hash_table_steal_extended (installed_actions, uuid, (gpointer *) &key, (gpointer *) &data)) + continue; + g_free (key); + data->enabled = !is_action_disabled_by_filename (data->filename, disabled_actions); + } else if (type == ROW_TYPE_SEPARATOR) { + data = row_data_new (); + data->uuid = g_strdup ("separator"); + data->type = ROW_TYPE_SEPARATOR; + + } else if (type == ROW_TYPE_SUBMENU) { + data = row_data_new (); + data->uuid = g_strdup (uuid); + data->type = ROW_TYPE_SUBMENU; + + } else { + continue; + } + + // optional fields + if (json_object_has_member (item, "user-label") && + !json_object_get_null_member (item, "user-label")) { + data->user_label = g_strdup (json_object_get_string_member (item, "user-label")); + } + + if (json_object_has_member (item, "user-icon") && + !json_object_get_null_member (item, "user-icon")) { + data->user_icon = g_strdup (json_object_get_string_member (item, "user-icon")); + } + + if (json_object_has_member (item, "accelerator") && + !json_object_get_null_member (item, "accelerator")) { + data->accelerator = g_strdup (json_object_get_string_member (item, "accelerator")); + } + + tree_store_append_row_data (self->priv->model, &iter, parent, data); + + if (type == ROW_TYPE_SUBMENU && + json_object_has_member (item, "children")) { + JsonArray *children = json_object_get_array_member (item, "children"); + fill_model_recursive (self, children, &iter, installed_actions, disabled_actions); + } + } +} + +static void +reload_model (NemoActionLayoutEditor *self, gboolean flat) +{ + GHashTable *installed_actions; + JsonParser *parser = NULL; + JsonObject *root_obj = NULL; + JsonArray *toplevel = NULL; + gchar **disabled_actions; + + self->priv->updating_model = TRUE; + gtk_tree_store_clear (self->priv->model); + + installed_actions = load_installed_actions (self); + + disabled_actions = g_settings_get_strv (self->priv->nemo_plugin_settings, + NEMO_PLUGIN_PREFERENCES_DISABLED_ACTIONS); + + if (!flat) { + gchar *json_file = g_build_filename (g_get_user_config_dir (), JSON_FILE, NULL); + + if (g_file_test (json_file, G_FILE_TEST_EXISTS)) { + parser = json_parser_new (); + + if (json_parser_load_from_file (parser, json_file, NULL)) { + JsonNode *root = json_parser_get_root (parser); + if (root && JSON_NODE_HOLDS_OBJECT (root)) { + root_obj = json_node_get_object (root); + if (json_object_has_member (root_obj, "toplevel")) { + toplevel = json_object_get_array_member (root_obj, "toplevel"); + } + } + } + } + + g_free (json_file); + } + + if (toplevel) { + fill_model_recursive (self, toplevel, NULL, installed_actions, disabled_actions); + } + + // Add remaining untracked actions to the end + + GHashTableIter hash_iter; + gpointer key, value; + g_hash_table_iter_init (&hash_iter, installed_actions); + + while (g_hash_table_iter_next (&hash_iter, &key, &value)) { + RowData *data = (RowData *) value; + GtkTreeIter iter; + + data->enabled = !is_action_disabled_by_filename (data->filename, disabled_actions); + + g_hash_table_iter_steal (&hash_iter); + g_free (key); + + tree_store_append_row_data (self->priv->model, &iter, NULL, data); + } + + g_strfreev (disabled_actions); + + if (parser) + g_object_unref (parser); + + g_hash_table_destroy (installed_actions); + + gtk_tree_view_expand_all (GTK_TREE_VIEW (self->priv->treeview)); + + GtkTreePath *first_path = gtk_tree_path_new_first (); + gtk_tree_selection_select_path (gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->treeview)), + first_path); + gtk_tree_path_free (first_path); + + update_row_controls (self); + update_arrow_button_states (self); + + self->priv->updating_model = FALSE; +} + +static void +set_needs_saved (NemoActionLayoutEditor *self, gboolean needs_saved) +{ + self->priv->needs_saved = needs_saved; + + gtk_widget_set_sensitive (self->priv->save_button, needs_saved); + gtk_widget_set_sensitive (self->priv->discard_button, needs_saved); +} + +static void +update_row_controls (NemoActionLayoutEditor *self) +{ + RowData *data; + RowType row_type; + GtkTreeIter iter; + + self->priv->updating_row_edit_fields = TRUE; + + data = get_selected_row_data (self, &iter); + + if (!data) { + gtk_widget_set_sensitive (self->priv->row_controls_box, FALSE); + gtk_entry_set_text (GTK_ENTRY (self->priv->name_entry), ""); + gtk_entry_set_icon_from_icon_name (GTK_ENTRY (self->priv->name_entry), + GTK_ENTRY_ICON_SECONDARY, NULL); + gtk_image_clear (GTK_IMAGE (self->priv->icon_selector_image)); + gtk_image_clear (GTK_IMAGE (self->priv->original_icon_menu_image)); + gtk_widget_hide (self->priv->original_icon_menu_item); + self->priv->updating_row_edit_fields = FALSE; + return; + } + + gtk_widget_set_sensitive (self->priv->row_controls_box, TRUE); + + gchar *label; + + gtk_tree_model_get (GTK_TREE_MODEL (self->priv->model), &iter, + COL_TYPE, &row_type, + -1); + + label = row_data_get_label (data); + gtk_entry_set_text (GTK_ENTRY (self->priv->name_entry), label); + g_free (label); + + gchar *icon_string = row_data_get_icon_string (data, FALSE); + if (icon_string) { + gtk_image_set_from_icon_name (GTK_IMAGE (self->priv->icon_selector_image), + icon_string, GTK_ICON_SIZE_BUTTON); + g_free (icon_string); + } else { + gtk_image_clear (GTK_IMAGE (self->priv->icon_selector_image)); + } + + // Original icon in menu - only show for actions + gboolean is_action = (row_type == ROW_TYPE_ACTION); + if (is_action) { + gchar *original_icon_string = row_data_get_icon_string (data, TRUE); + if (original_icon_string) { + gtk_image_set_from_icon_name (GTK_IMAGE (self->priv->original_icon_menu_image), + original_icon_string, GTK_ICON_SIZE_MENU); + gtk_widget_set_sensitive (self->priv->original_icon_menu_item, TRUE); + g_free (original_icon_string); + } else { + gtk_image_clear (GTK_IMAGE (self->priv->original_icon_menu_image)); + gtk_widget_set_sensitive (self->priv->original_icon_menu_item, FALSE); + } + gtk_widget_show (self->priv->original_icon_menu_item); + } else { + gtk_widget_hide (self->priv->original_icon_menu_item); + } + + gboolean is_separator = (row_type == ROW_TYPE_SEPARATOR); + xapp_visibility_group_set_sensitive (self->priv->selected_item_widgets_group, + data->enabled && !is_separator); + + gtk_widget_set_sensitive (self->priv->remove_submenu_button, + row_type != ROW_TYPE_ACTION); + + // Show clear custom label icon if applicable + if (row_type == ROW_TYPE_ACTION && data->user_label) { + gtk_entry_set_icon_from_icon_name (GTK_ENTRY (self->priv->name_entry), + GTK_ENTRY_ICON_SECONDARY, + "edit-delete-symbolic"); + } else { + gtk_entry_set_icon_from_icon_name (GTK_ENTRY (self->priv->name_entry), + GTK_ENTRY_ICON_SECONDARY, NULL); + } + + row_data_free (data); + self->priv->updating_row_edit_fields = FALSE; +} + +static void +update_arrow_button_states (NemoActionLayoutEditor *self) +{ + GtkTreeIter iter, first_iter; + GtkTreePath *path; + gboolean can_up = TRUE, can_down = TRUE; + + if (!get_selected_row (self, &path, &iter)) { + gtk_widget_set_sensitive (self->priv->up_button, FALSE); + gtk_widget_set_sensitive (self->priv->down_button, FALSE); + return; + } + + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->model), &first_iter)) { + GtkTreePath *first_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &first_iter); + if (gtk_tree_path_compare (path, first_path) == 0) { + can_up = FALSE; + } + gtk_tree_path_free (first_path); + } + + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->model), &first_iter)) { + GtkTreeIter last_top = first_iter; + GtkTreeIter temp = first_iter; + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->priv->model), &temp)) { + last_top = temp; + } + + GtkTreeIter absolute_last = get_last_at_level (GTK_TREE_MODEL (self->priv->model), &last_top); + GtkTreePath *last_path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->priv->model), &absolute_last); + + if (last_path) { + if (gtk_tree_path_compare (path, last_path) == 0) { + can_down = FALSE; + } + gtk_tree_path_free (last_path); + } + } + + gtk_widget_set_sensitive (self->priv->up_button, can_up); + gtk_widget_set_sensitive (self->priv->down_button, can_down); + + gtk_tree_path_free (path); +} + +static void +selected_row_changed (NemoActionLayoutEditor *self, gboolean needs_saved) +{ + GtkTreePath *path; + GtkTreeIter iter; + + if (self->priv->updating_model) + return; + + if (get_selected_row (self, &path, &iter)) { + gtk_tree_model_row_changed (GTK_TREE_MODEL (self->priv->model), path, &iter); + gtk_tree_path_free (path); + } + + update_row_controls (self); + update_arrow_button_states (self); + + if (needs_saved) + set_needs_saved (self, TRUE); +} + +static void +on_selection_changed (GtkTreeSelection *selection, NemoActionLayoutEditor *self) +{ + if (self->priv->updating_model) + return; + + update_row_controls (self); + update_arrow_button_states (self); + + gtk_widget_queue_draw (self->priv->treeview); +} + +static void +nemo_action_layout_editor_dispose (GObject *object) +{ + NemoActionLayoutEditor *self = NEMO_ACTION_LAYOUT_EDITOR (object); + + g_clear_object (&self->priv->model); + g_clear_object (&self->priv->nemo_plugin_settings); + g_clear_pointer (&self->priv->selected_item_widgets_group, xapp_visibility_group_free); + + if (self->priv->dir_monitors) { + g_list_free_full (self->priv->dir_monitors, g_object_unref); + self->priv->dir_monitors = NULL; + } + + if (self->priv->builtin_shortcuts) { + g_list_free_full (self->priv->builtin_shortcuts, (GDestroyNotify) builtin_shortcut_free); + self->priv->builtin_shortcuts = NULL; + } + + G_OBJECT_CLASS (nemo_action_layout_editor_parent_class)->dispose (object); +} + +static void +nemo_action_layout_editor_finalize (GObject *object) +{ + G_OBJECT_CLASS (nemo_action_layout_editor_parent_class)->finalize (object); +} + +static void +nemo_action_layout_editor_class_init (NemoActionLayoutEditorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = nemo_action_layout_editor_dispose; + object_class->finalize = nemo_action_layout_editor_finalize; +} + +static void +nemo_action_layout_editor_init (NemoActionLayoutEditor *self) +{ + GtkBuilder *builder; + GtkWidget *main_box, *menu, *item; + GtkTreeViewColumn *column; + GtkCellRenderer *cell; + GtkTreeSelection *selection; + PangoLayout *layout; + gint layout_w, layout_h; + + self->priv = nemo_action_layout_editor_get_instance_private (self); + + self->priv->needs_saved = FALSE; + self->priv->updating_model = FALSE; + self->priv->updating_row_edit_fields = FALSE; + self->priv->dnd_autoscroll_timeout_id = 0; + + self->priv->nemo_plugin_settings = g_settings_new ("org.nemo.plugins"); + self->priv->settings_handler_id = g_signal_connect (self->priv->nemo_plugin_settings, + "changed", + G_CALLBACK (on_disabled_settings_list_changed), + self); + + self->priv->builtin_shortcuts = NULL; + load_builtin_shortcuts (self); + + builder = gtk_builder_new_from_resource ("/org/nemo/action-layout-editor/nemo-action-layout-editor.glade"); + main_box = GTK_WIDGET (gtk_builder_get_object (builder, "layout_editor_box")); + gtk_box_pack_start (GTK_BOX (self), main_box, TRUE, TRUE, 0); + + self->priv->scrolled_window = GTK_WIDGET (gtk_builder_get_object (builder, "treeview_holder")); + self->priv->save_button = GTK_WIDGET (gtk_builder_get_object (builder, "save_button")); + self->priv->discard_button = GTK_WIDGET (gtk_builder_get_object (builder, "discard_changes_button")); + self->priv->default_layout_button = GTK_WIDGET (gtk_builder_get_object (builder, "default_layout_button")); + self->priv->row_controls_box = GTK_WIDGET (gtk_builder_get_object (builder, "row_controls_box")); + self->priv->new_row_button = GTK_WIDGET (gtk_builder_get_object (builder, "new_row_button")); + self->priv->remove_submenu_button = GTK_WIDGET (gtk_builder_get_object (builder, "remove_submenu_button")); + self->priv->up_button = GTK_WIDGET (gtk_builder_get_object (builder, "up_button")); + self->priv->down_button = GTK_WIDGET (gtk_builder_get_object (builder, "down_button")); + self->priv->icon_selector_menu_button = GTK_WIDGET (gtk_builder_get_object (builder, "icon_selector_menu_button")); + self->priv->icon_selector_image = GTK_WIDGET (gtk_builder_get_object (builder, "icon_selector_image")); + self->priv->name_entry = GTK_WIDGET (gtk_builder_get_object (builder, "name_entry")); + + // The store owns all of this data. Whenever we get a RowData from the store, + // it's a copy and must be freed. Whenever we add a RowData to the store, it copies + // it, we must free the one we made (just like strings). + self->priv->model = gtk_tree_store_new (N_COLUMNS, + G_TYPE_STRING, // hash + G_TYPE_STRING, // uuid + G_TYPE_INT, // RowType + ROW_DATA_TYPE); // RowData + + self->priv->treeview = gtk_tree_view_new_with_model (GTK_TREE_MODEL (self->priv->model)); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self->priv->treeview), FALSE); + gtk_tree_view_set_enable_tree_lines (GTK_TREE_VIEW (self->priv->treeview), TRUE); + gtk_container_add (GTK_CONTAINER (self->priv->scrolled_window), self->priv->treeview); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_expand (column, TRUE); + gtk_tree_view_column_set_spacing (column, 2); + gtk_tree_view_append_column (GTK_TREE_VIEW (self->priv->treeview), column); + + cell = gtk_cell_renderer_toggle_new (); + g_object_set (cell, "activatable", TRUE, NULL); + g_signal_connect (cell, "toggled", G_CALLBACK (on_action_row_toggled), self); + gtk_tree_view_column_pack_start (column, cell, FALSE); + gtk_tree_view_column_set_cell_data_func (column, cell, toggle_render_func, self, NULL); + + cell = gtk_cell_renderer_pixbuf_new (); + gtk_tree_view_column_pack_start (column, cell, FALSE); + gtk_tree_view_column_set_cell_data_func (column, cell, menu_icon_render_func, self, NULL); + + cell = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, cell, FALSE); + gtk_tree_view_column_set_cell_data_func (column, cell, menu_label_render_func, self, NULL); + + column = gtk_tree_view_column_new (); + gtk_tree_view_column_set_expand (column, TRUE); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); + gtk_tree_view_append_column (GTK_TREE_VIEW (self->priv->treeview), column); + + cell = gtk_cell_renderer_accel_new (); + g_object_set (cell, + "editable", TRUE, + "xalign", 0.0, + NULL); + gtk_tree_view_column_pack_end (column, cell, FALSE); + gtk_tree_view_column_set_cell_data_func (column, cell, accel_render_func, self, NULL); + + layout = gtk_widget_create_pango_layout (GTK_WIDGET (self->priv->treeview), _("Click to add a shortcut")); + pango_layout_get_pixel_size (layout, &layout_w, &layout_h); + gtk_tree_view_column_set_min_width (column, layout_w); + g_object_unref (layout); + + g_signal_connect (cell, "editing-started", G_CALLBACK (on_accel_edit_started), self); + g_signal_connect (cell, "accel-edited", G_CALLBACK (on_accel_edited), self); + g_signal_connect (cell, "accel-cleared", G_CALLBACK (on_accel_cleared), self); + self->priv->editing_accel = FALSE; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->treeview)); + g_signal_connect (selection, "changed", G_CALLBACK (on_selection_changed), self); + g_signal_connect (self->priv->treeview, "row-activated", G_CALLBACK (on_row_activated), self); + + gtk_drag_source_set (self->priv->treeview, + GDK_BUTTON1_MASK, + NULL, 0, + GDK_ACTION_MOVE); + gtk_drag_dest_set (self->priv->treeview, + GTK_DEST_DEFAULT_ALL, + NULL, 0, + GDK_ACTION_MOVE); + + gtk_drag_source_add_text_targets (GTK_WIDGET (self->priv->treeview)); + gtk_drag_dest_add_text_targets (GTK_WIDGET (self->priv->treeview)); + g_signal_connect (self->priv->treeview, "drag-begin", G_CALLBACK (on_drag_begin), self); + g_signal_connect (self->priv->treeview, "drag-end", G_CALLBACK (on_drag_end), self); + g_signal_connect (self->priv->treeview, "drag-motion", G_CALLBACK (on_drag_motion), self); + g_signal_connect (self->priv->treeview, "drag-data-get", G_CALLBACK (on_drag_data_get), self); + g_signal_connect (self->priv->treeview, "drag-data-received", G_CALLBACK (on_drag_data_received), self); + + menu = gtk_menu_new (); + item = gtk_menu_item_new_with_label (_("New submenu")); + g_signal_connect (item, "activate", G_CALLBACK (on_new_submenu_clicked), self); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + item = gtk_menu_item_new_with_label (_("New separator")); + g_signal_connect (item, "activate", G_CALLBACK (on_new_separator_clicked), self); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + gtk_widget_show_all (menu); + gtk_menu_button_set_popup (GTK_MENU_BUTTON (self->priv->new_row_button), menu); + + menu = gtk_menu_new (); + + GtkWidget *image = gtk_image_new_from_icon_name ("xsi-checkbox-symbolic", GTK_ICON_SIZE_MENU); + item = gtk_image_menu_item_new_with_label (_("No icon")); + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image); + g_signal_connect (item, "activate", G_CALLBACK (on_clear_icon_clicked), self); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + + self->priv->original_icon_menu_image = gtk_image_new (); + self->priv->original_icon_menu_item = gtk_image_menu_item_new_with_label (_("Use the original icon (if there is one)")); + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (self->priv->original_icon_menu_item), + self->priv->original_icon_menu_image); + g_signal_connect (self->priv->original_icon_menu_item, "activate", G_CALLBACK (on_original_icon_clicked), self); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), self->priv->original_icon_menu_item); + + item = gtk_menu_item_new_with_label (_("Choose...")); + g_signal_connect (item, "activate", G_CALLBACK (on_choose_icon_clicked), self); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + + gtk_widget_show_all (menu); + gtk_menu_button_set_popup (GTK_MENU_BUTTON (self->priv->icon_selector_menu_button), menu); + + g_signal_connect (self->priv->save_button, "clicked", G_CALLBACK (on_save_clicked), self); + g_signal_connect (self->priv->discard_button, "clicked", G_CALLBACK (on_discard_changes_clicked), self); + g_signal_connect (self->priv->default_layout_button, "clicked", G_CALLBACK (on_default_layout_clicked), self); + g_signal_connect (self->priv->remove_submenu_button, "clicked", G_CALLBACK (on_remove_submenu_clicked), self); + g_signal_connect (self->priv->up_button, "clicked", G_CALLBACK (on_up_button_clicked), self); + g_signal_connect (self->priv->down_button, "clicked", G_CALLBACK (on_down_button_clicked), self); + g_signal_connect (self->priv->name_entry, "changed", G_CALLBACK (on_name_entry_changed), self); + g_signal_connect (self->priv->name_entry, "icon-press", G_CALLBACK (on_name_entry_icon_press), self); + + self->priv->selected_item_widgets_group = xapp_visibility_group_new (TRUE, TRUE, NULL); + xapp_visibility_group_add_widget (self->priv->selected_item_widgets_group, + self->priv->icon_selector_menu_button); + xapp_visibility_group_add_widget (self->priv->selected_item_widgets_group, + self->priv->name_entry); + + g_object_unref (builder); + + gtk_widget_show_all (GTK_WIDGET (self)); + + reload_model (self, FALSE); + set_needs_saved (self, FALSE); +} + +GtkWidget * +nemo_action_layout_editor_new (void) +{ + return g_object_new (NEMO_TYPE_ACTION_LAYOUT_EDITOR, NULL); +} diff --git a/libnemo-extension/nemo-action-layout-editor.h b/libnemo-extension/nemo-action-layout-editor.h new file mode 100644 index 000000000..ecef7e4f1 --- /dev/null +++ b/libnemo-extension/nemo-action-layout-editor.h @@ -0,0 +1,22 @@ +/* nemo-action-layout-editor.h */ + +/* A widget that allows organizing nemo actions into a tree structure + * with submenus and separators, and assigning keyboard shortcuts. + */ + +#ifndef __NEMO_ACTION_LAYOUT_EDITOR_H__ +#define __NEMO_ACTION_LAYOUT_EDITOR_H__ + +#include + +G_BEGIN_DECLS + +#define NEMO_TYPE_ACTION_LAYOUT_EDITOR (nemo_action_layout_editor_get_type()) + +G_DECLARE_FINAL_TYPE (NemoActionLayoutEditor, nemo_action_layout_editor, NEMO, ACTION_LAYOUT_EDITOR, GtkBox) + +GtkWidget *nemo_action_layout_editor_new (void); + +G_END_DECLS + +#endif /* __NEMO_ACTION_LAYOUT_EDITOR_H__ */ diff --git a/libnemo-extension/test-action-layout-editor.c b/libnemo-extension/test-action-layout-editor.c new file mode 100644 index 000000000..368929589 --- /dev/null +++ b/libnemo-extension/test-action-layout-editor.c @@ -0,0 +1,30 @@ +#include +#include "nemo-action-layout-editor.h" + +int main (int argc, char *argv[]) +{ + GtkWidget *window; + GtkWidget *editor; + + gtk_init (&argc, &argv); + + /* Create main window */ + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (window), "Nemo Action Layout Editor Test"); + gtk_window_set_default_size (GTK_WINDOW (window), 800, 600); + g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL); + + /* Create editor widget */ + editor = nemo_action_layout_editor_new (); + gtk_container_set_border_width (GTK_CONTAINER (window), 6); + gtk_container_add (GTK_CONTAINER (window), editor); + + /* Show everything */ + gtk_widget_show_all (window); + gtk_window_present (GTK_WINDOW (window)); + + /* Run */ + gtk_main (); + + return 0; +} diff --git a/libnemo-private/nemo-debug.c b/libnemo-private/nemo-debug.c index 94eecebc6..d11f77d2c 100644 --- a/libnemo-private/nemo-debug.c +++ b/libnemo-private/nemo-debug.c @@ -55,6 +55,7 @@ static GDebugKey keys[] = { { "Thumbnails", NEMO_DEBUG_THUMBNAILS }, { "Search", NEMO_DEBUG_SEARCH }, { "Preferences", NEMO_DEBUG_PREFERENCES }, + { "ActionLayoutEditor", NEMO_DEBUG_ACTION_LAYOUT_EDITOR }, { 0, } }; diff --git a/libnemo-private/nemo-debug.h b/libnemo-private/nemo-debug.h index 7aa6e114d..adcd797cc 100644 --- a/libnemo-private/nemo-debug.h +++ b/libnemo-private/nemo-debug.h @@ -51,7 +51,8 @@ typedef enum { NEMO_DEBUG_DESKTOP = 1 << 16, NEMO_DEBUG_THUMBNAILS = 1 << 17, NEMO_DEBUG_SEARCH = 1 << 18, - NEMO_DEBUG_PREFERENCES = 1 << 19 + NEMO_DEBUG_PREFERENCES = 1 << 19, + NEMO_DEBUG_ACTION_LAYOUT_EDITOR = 1 << 20 } DebugFlags; void nemo_debug_set_flags (DebugFlags flags); diff --git a/src/meson.build b/src/meson.build index 07a56957d..4df10b5b7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -51,7 +51,6 @@ nemoCommon_sources = [ 'nemo-notebook.c', 'nemo-pathbar.c', 'nemo-places-sidebar.c', - 'nemo-plugin-manager.c', 'nemo-previewer.c', 'nemo-progress-info-widget.c', 'nemo-progress-ui-handler.c', diff --git a/src/nemo-blank-desktop-window.c b/src/nemo-blank-desktop-window.c index f96750ac4..3bb816651 100644 --- a/src/nemo-blank-desktop-window.c +++ b/src/nemo-blank-desktop-window.c @@ -39,8 +39,6 @@ #include -#include "nemo-plugin-manager.h" - #define DEBUG_FLAG NEMO_DEBUG_DESKTOP #include diff --git a/src/nemo-file-management-properties.c b/src/nemo-file-management-properties.c index d0a1958e1..5ec05ca67 100644 --- a/src/nemo-file-management-properties.c +++ b/src/nemo-file-management-properties.c @@ -40,8 +40,10 @@ #include #include -#include "nemo-plugin-manager.h" #include "nemo-template-config-widget.h" +#include "nemo-action-layout-editor.h" +#include "nemo-extension-config-widget.h" + #include "nemo-actions.h" /* string enum preferences */ @@ -464,14 +466,14 @@ nemo_file_management_properties_dialog_setup_icon_caption_page (GtkBuilder *buil } static void -nemo_file_management_properties_dialog_setup_plugin_page (GtkBuilder *builder) +nemo_file_management_properties_dialog_setup_extensions_page (GtkBuilder *builder) { GtkWidget *box; - box = GTK_WIDGET (gtk_builder_get_object (builder, "plugin_box")); + box = GTK_WIDGET (gtk_builder_get_object (builder, "extensions_box")); gtk_box_pack_start (GTK_BOX (box), - GTK_WIDGET (nemo_plugin_manager_new ()), + GTK_WIDGET (nemo_extension_config_widget_new ()), TRUE, TRUE, 0); } @@ -487,6 +489,18 @@ nemo_file_management_properties_dialog_setup_templates_page (GtkBuilder *builder TRUE, TRUE, 0); } +static void +nemo_file_management_properties_dialog_setup_actions_page (GtkBuilder *builder) +{ + GtkWidget *box; + + box = GTK_WIDGET (gtk_builder_get_object (builder, "actions_box")); + + gtk_box_pack_start (GTK_BOX (box), + GTK_WIDGET (nemo_action_layout_editor_new ()), + TRUE, TRUE, 0); +} + static void create_date_format_menu (GtkBuilder *builder) { @@ -1142,8 +1156,9 @@ nemo_file_management_properties_dialog_setup (GtkBuilder *builder, nemo_file_management_properties_dialog_setup_icon_caption_page (builder); nemo_file_management_properties_dialog_setup_list_column_page (builder); - nemo_file_management_properties_dialog_setup_plugin_page (builder); + nemo_file_management_properties_dialog_setup_extensions_page (builder); nemo_file_management_properties_dialog_setup_templates_page (builder); + nemo_file_management_properties_dialog_setup_actions_page (builder); setup_configurable_menu_items (builder); diff --git a/src/nemo-plugin-manager.c b/src/nemo-plugin-manager.c deleted file mode 100644 index c68136da8..000000000 --- a/src/nemo-plugin-manager.c +++ /dev/null @@ -1,58 +0,0 @@ -/* nemo-plugin-manager.c */ - -/* A GtkWidget that can be inserted into a UI that provides a simple interface for - * managing the loading of extensions, actions and scripts - */ - -#include -#include "nemo-plugin-manager.h" -#include "nemo-action-config-widget.h" -#include "nemo-template-config-widget.h" -#include "nemo-extension-config-widget.h" -#include - -struct _NemoPluginManager -{ - GtkBin parent_instance; -}; - -G_DEFINE_TYPE (NemoPluginManager, nemo_plugin_manager, GTK_TYPE_BIN); - -static void -nemo_plugin_manager_class_init (NemoPluginManagerClass *klass) -{ -} - -static void -nemo_plugin_manager_init (NemoPluginManager *self) -{ - GtkWidget *widget, *grid; - - grid = gtk_grid_new (); - - gtk_widget_set_margin_left (grid, 4); - gtk_widget_set_margin_right (grid, 4); - gtk_widget_set_margin_top (grid, 4); - gtk_widget_set_margin_bottom (grid, 4); - gtk_grid_set_row_spacing (GTK_GRID (grid), 4); - gtk_grid_set_column_spacing (GTK_GRID (grid), 4); - gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE); - gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE); - - widget = nemo_action_config_widget_new (); - gtk_grid_attach (GTK_GRID (grid), widget, 0, 0, 2, 1); - - widget = nemo_extension_config_widget_new (); - gtk_grid_attach (GTK_GRID (grid), widget, 0, 1, 2, 1); - - gtk_container_add (GTK_CONTAINER (self), grid); - - gtk_widget_show_all (GTK_WIDGET (self)); -} - -NemoPluginManager * -nemo_plugin_manager_new (void) -{ - return g_object_new (NEMO_TYPE_PLUGIN_MANAGER, NULL); -} - diff --git a/src/nemo-plugin-manager.h b/src/nemo-plugin-manager.h deleted file mode 100644 index a41993f37..000000000 --- a/src/nemo-plugin-manager.h +++ /dev/null @@ -1,23 +0,0 @@ -/* nemo-plugin-manager.h */ - -/* A GtkWidget that can be inserted into a UI that provides a simple interface for - * managing the loading of extensions, actions and scripts - */ - -#ifndef __NEMO_PLUGIN_MANAGER_H__ -#define __NEMO_PLUGIN_MANAGER_H__ - -#include -#include - -G_BEGIN_DECLS - -#define NEMO_TYPE_PLUGIN_MANAGER (nemo_plugin_manager_get_type()) - -G_DECLARE_FINAL_TYPE (NemoPluginManager, nemo_plugin_manager, NEMO, PLUGIN_MANAGER, GtkBin) - -NemoPluginManager *nemo_plugin_manager_new (void); - -G_END_DECLS - -#endif /* __NEMO_PLUGIN_MANAGER_H__ */ diff --git a/src/nemo-window-menus.c b/src/nemo-window-menus.c index 46f10b8e6..76033e7f2 100644 --- a/src/nemo-window-menus.c +++ b/src/nemo-window-menus.c @@ -301,7 +301,7 @@ action_preferences_callback (GtkAction *action, window = GTK_WINDOW (user_data); - nemo_file_management_properties_dialog_show (window, NULL); + nemo_file_management_properties_dialog_show (NULL, NULL); } static void