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
-
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
-
-
-
-
-
-
-
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