From b74e8e0b04a6a3f2eaedee0dd8d28d0af83ba0c5 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Tue, 30 Jan 2024 14:05:37 -0500 Subject: [PATCH 1/2] Implement submenus for actions. - allow submenus, separators - allow nested menus - menu editor - drag-and-drop - allow submenus to have icons - allow overriding action labels, icons - enable/disable actions - refactor blank-desktop-view, treeview-sidebar and places- sidebar to utilize GtkActions - refactor action manager to work everywhere, not just the primary view - refactor nemo-actions.c to clean up, reduce code. --- action-layout-editor/actions-tree.md | 99 ++ action-layout-editor/leconfig.py.in | 6 + action-layout-editor/meson.build | 36 + .../nemo-action-layout-editor.glade | 377 ++++++ .../nemo-action-layout-editor.in | 2 + .../nemo-action-layout-editor.py | 1061 +++++++++++++++++ debian/control | 1 + gresources/nemo-blank-desktop-window-ui.xml | 7 + gresources/nemo-places-sidebar-ui.xml | 23 + gresources/nemo-tree-sidebar-ui.xml | 25 + gresources/nemo.gresource.xml | 3 + libnemo-private/meson.build | 2 +- libnemo-private/nemo-action-manager.c | 521 +++++++- libnemo-private/nemo-action-manager.h | 61 +- libnemo-private/nemo-action-symbols.h | 101 ++ libnemo-private/nemo-action.c | 598 ++++------ libnemo-private/nemo-action.h | 122 +- libnemo-private/nemo-ui-utilities.c | 13 + libnemo-private/nemo-ui-utilities.h | 2 +- meson.build | 2 + src/nemo-action-config-widget.c | 16 +- src/nemo-actions.h | 3 + src/nemo-blank-desktop-window.c | 297 +++-- src/nemo-places-sidebar.c | 626 ++++------ src/nemo-tree-sidebar.c | 933 +++++++-------- src/nemo-view.c | 153 +-- 26 files changed, 3463 insertions(+), 1627 deletions(-) create mode 100644 action-layout-editor/actions-tree.md create mode 100644 action-layout-editor/leconfig.py.in create mode 100644 action-layout-editor/meson.build create mode 100644 action-layout-editor/nemo-action-layout-editor.glade create mode 100644 action-layout-editor/nemo-action-layout-editor.in create mode 100644 action-layout-editor/nemo-action-layout-editor.py create mode 100644 gresources/nemo-blank-desktop-window-ui.xml create mode 100644 gresources/nemo-places-sidebar-ui.xml create mode 100644 gresources/nemo-tree-sidebar-ui.xml create mode 100644 libnemo-private/nemo-action-symbols.h diff --git a/action-layout-editor/actions-tree.md b/action-layout-editor/actions-tree.md new file mode 100644 index 000000000..2a73ceef7 --- /dev/null +++ b/action-layout-editor/actions-tree.md @@ -0,0 +1,99 @@ + +## Actions layout JSON format + +- The layout file is saved as `~/.config/nemo/actions/actions-tree.json`. +- A missing or invalid layout will result in a flat list of all actions. +- Definition requirements may change... + +#### Structure and node elements + +##### The definition must have a 'toplevel' root object, which contains an array of objects (with these possibly having arrays of children also): +```json +{ + "toplevel": [ + ] +} +``` + +##### Children must adhere to the following definitions: +- `'uuid': string` - For valid 'spices'-based actions, this will be their UUID. For legacy/non-spice actions, it will be the action file's basename, suffixed by `@untracked`. For submenus, it will be the submenu's label. +- `'type': string` - `action`, `submenu` or `separator` +- `'position': integer` - The action or submenu's position at the current tree depth. This is currently for reference only, as the order is preserved when parsing or creating json files, and saved position is ignored. +- `'user-label': string` - can be `null` - The action or submenu's label. In the case of actions, this will be `null` initially, and the action's `Name` field will be used. It can be overridden, and the new value is kept here. +- `'user-icon': string` - can be `null` or empty - The action or submenu's icon. In the case of actions, this will be `null` initially, and the icon string will be drawn from the action's `Icon-Name` field. It can be overridden - the new value is kept here. The special value of `""` (empty string) will suppress any icon altogether. +- `'children': array` (submenu types only) - contains another level of actions and possibly submenus. + +##### Example +```json +{ + "toplevel": [ + { + "uuid": "sample@untracked", + "type": "action", + "position": 0, + "user-label": null, + "user-icon": null + }, + { + "uuid": "mint_dev_tool_make_thumbnail@untracked", + "type": "action", + "position": 1, + "user-label": null, + "user-icon": null + }, + { + "uuid": "92_show-expo@untracked", + "type": "action", + "position": 2, + "user-label": "Manage workspaces", + "user-icon": "address-book-new-symbolic" + }, + { + "uuid": "Test category", + "type": "submenu", + "position": 3, + "user-label": "Test category", + "user-icon": "face-smile", + "children": [ + { + "uuid": "change-background@untracked", + "type": "action", + "position": 0, + "user-label": null, + "user-icon": null + }, + { + "uuid": "Sub test category", + "type": "submenu", + "position": 1, + "user-label": "Sub test category", + "user-icon": null, + "children": [ + { + "uuid": "mint_dev_tool_show_file_metadata@untracked", + "type": "action", + "position": 0, + "user-label": null, + "user-icon": null + } + ] + }, + { + "uuid": "mint_dev_tool_add_shadow@untracked", + "type": "action", + "position": 2, + "user-label": null, + "user-icon": null + } + ] + }, + { + "uuid": "91_delete-workspace@untracked", + "type": "action", + "position": 4, + "user-label": null, + "user-icon": null + } + ] +} +``` diff --git a/action-layout-editor/leconfig.py.in b/action-layout-editor/leconfig.py.in new file mode 100644 index 000000000..25ca68e8b --- /dev/null +++ b/action-layout-editor/leconfig.py.in @@ -0,0 +1,6 @@ +# Generated file - DO NOT EDIT. Edit config.py.in instead. + +LOCALE_DIR=@LOCALE_DIR@ +PACKAGE=@PACKAGE@ +VERSION=@VERSION@ +PKG_DATADIR=@PKG_DATADIR@ diff --git a/action-layout-editor/meson.build b/action-layout-editor/meson.build new file mode 100644 index 000000000..bcc4465da --- /dev/null +++ b/action-layout-editor/meson.build @@ -0,0 +1,36 @@ +conf = configuration_data() +conf.set_quoted('PKG_DATADIR', nemoDataPath) +conf.set_quoted('LOCALE_DIR', join_paths(get_option('prefix'), get_option('localedir'))) +conf.set_quoted('PACKAGE', meson.project_name()) +conf.set_quoted('VERSION', meson.project_version()) + +config_py = configure_file( + input: 'leconfig.py.in', + output: 'leconfig.py', + configuration: conf, + install: true, + install_dir: nemoDataPath / 'layout-editor', +) + +bin_conf = configuration_data() +bin_conf.set('PKG_DATADIR', nemoDataPath) + +bin = configure_file( + input: 'nemo-action-layout-editor.in', + output: 'nemo-action-layout-editor', + configuration: bin_conf, + install: true, + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x' +) + +install_data( + 'nemo-action-layout-editor.py', + install_dir: nemoDataPath / 'layout-editor', + install_mode: 'rwxr-xr-x' +) + +install_data( + 'nemo-action-layout-editor.glade', + install_dir: nemoDataPath / 'layout-editor' +) diff --git a/action-layout-editor/nemo-action-layout-editor.glade b/action-layout-editor/nemo-action-layout-editor.glade new file mode 100644 index 000000000..b65811b4c --- /dev/null +++ b/action-layout-editor/nemo-action-layout-editor.glade @@ -0,0 +1,377 @@ + + + + + + True + False + list-remove-symbolic + + + False + 600 + 400 + nemo + + + True + False + 10 + 10 + 10 + 10 + vertical + 6 + + + True + False + + + True + False + 0 + in + + + True + False + + + True + False + vertical + + + True + True + True + in + + + + + + True + True + 0 + + + + + True + False + False + 2 + + + True + False + + + True + False + + + True + True + False + True + + + True + False + list-add-symbolic + + + + + False + True + 0 + + + + + True + True + True + image2 + + + False + True + 1 + + + + + + + + True + True + + + + + True + False + + + True + False + 6 + + + True + False + + + True + True + center + center + + + False + True + 2 + + + + + False + True + 1 + + + + + True + False + + + True + True + False + True + + + True + False + + + + + False + True + 0 + + + + + True + True + center + 36 + edit-delete-symbolic + False + False + Restore this action's original label. + + + True + True + 1 + + + + + + True + True + 2 + + + + + + + + True + True + + + + + False + True + 1 + + + + + True + False + False + 2 + + + True + False + + + True + False + start + + + True + False + foo@bar + + + + + + True + True + 0 + + + + + + + True + True + + + + + False + True + 2 + + + + + + + + + + + + True + True + 0 + + + + + False + True + 0 + + + + + + + + True + False + + + + + + False + True + 2 + + + + + True + False + + + Discard changes + True + True + True + + + True + True + 0 + + + + + Save Layout + True + True + True + + + + True + True + 1 + + + + + False + True + end + 3 + + + + + + + True + False + Nemo Actions Layout Editor + False + True + + + True + True + False + True + + + True + False + open-menu-symbolic + + + + + + + + + vertical + + + + + + diff --git a/action-layout-editor/nemo-action-layout-editor.in b/action-layout-editor/nemo-action-layout-editor.in new file mode 100644 index 000000000..ab7cc02f3 --- /dev/null +++ b/action-layout-editor/nemo-action-layout-editor.in @@ -0,0 +1,2 @@ +#!/bin/sh +exec @PKG_DATADIR@/layout-editor/nemo-action-layout-editor.py diff --git a/action-layout-editor/nemo-action-layout-editor.py b/action-layout-editor/nemo-action-layout-editor.py new file mode 100644 index 000000000..2d5d52ee3 --- /dev/null +++ b/action-layout-editor/nemo-action-layout-editor.py @@ -0,0 +1,1061 @@ +#!/usr/bin/python3 +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('XApp', '1.0') +from gi.repository import Gtk, Gdk, GLib, Gio, XApp, GdkPixbuf, Pango +import cairo +import json +import os +from pathlib import Path +import uuid +import gettext +import subprocess + +import leconfig + +#FIXME build config +gettext.install(leconfig.PACKAGE, leconfig.LOCALE_DIR) + +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") +GLADE_FILE = Path(leconfig.PKG_DATADIR).joinpath("layout-editor/nemo-action-layout-editor.glade") + +NON_SPICE_UUID_SUFFIX = "@untracked" + +ROW_HASH, ROW_UUID, ROW_TYPE, ROW_POSITION, ROW_OBJ = range(5) + +ROW_TYPE_ACTION = "action" +ROW_TYPE_SUBMENU = "submenu" +ROW_TYPE_SEPARATOR = "separator" + +def new_hash(): + return uuid.uuid4().hex + +class Row(): + def __init__(self, row_meta=None, keyfile=None, path=None, enabled=True): + self.keyfile = keyfile + self.row_meta = row_meta + self.enabled = enabled + 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("/"): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_string, 16, 16) + surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, self.main_window.get_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_string('Nemo Action', 'Name').replace("_", "") + except GLib.Error as e: + print(e) + pass + + if label is None: + return _("Unknown") + + return label + + 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 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(): + def __init__(self): + self.builder = Gtk.Builder.new_from_file(str(GLADE_FILE)) + + self.main_window = self.builder.get_object("main_window") + 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.name_entry = self.builder.get_object("name_entry") + self.new_row_button = self.builder.get_object("new_row_button") + self.remove_submenu_button = self.builder.get_object("remove_submenu_button") + self.hamburger_button = self.builder.get_object("hamburger_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.action_enabled_switch = self.builder.get_object("action_enabled_switch") + self.row_uuid_label = self.builder.get_object("row_uuid_label") + self.selected_item_widgets_group = XApp.VisibilityGroup.new(True, True, [ + self.icon_selector_menu_button, + self.name_entry + ]) + + self.nemo_plugin_settings = Gio.Settings(schema_id="org.nemo.plugins") + + # Hamburger menu + menu = Gtk.Menu() + + item = Gtk.ImageMenuItem(label=_("Open user actions folder"), image=Gtk.Image(icon_name="folder-symbolic", icon_size=Gtk.IconSize.MENU)) + item.connect("activate", self.open_actions_folder_clicked) + menu.add(item) + + item = Gtk.ImageMenuItem(label=_("Reset layout"), image=Gtk.Image(icon_name="view-sort-ascending-symbolic", icon_size=Gtk.IconSize.MENU)) + item.connect("activate", self.flatten_layout_clicked) + menu.add(item) + + item = Gtk.MenuItem(label=_("Quit")) + item.connect("activate", self.quit) + menu.add(item) + + menu.show_all() + self.hamburger_button.set_popup(menu) + + # Icon MenuButton + menu = Gtk.Menu() + + self.blank_icon_menu_item = Gtk.ImageMenuItem(label=_("No icon"), image=Gtk.Image(icon_name="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="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="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.model = Gtk.TreeStore(str, str, str, int, object) # (hash, uuid, type, position, Row) + + self.treeview = Gtk.TreeView( + model=self.model, + enable_tree_lines=True, + headers_visible=False, + visible=True + ) + + column = Gtk.TreeViewColumn() + self.treeview.append_column(column) + + 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) + + self.treeview_holder.add(self.treeview) + + self.main_window.connect("delete-event", self.window_delete) + self.save_button.connect("clicked", self.on_save_clicked) + self.discard_changes_button.connect("clicked", self.on_discard_changes_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.action_enabled_switch.connect("notify::active", self.on_action_enabled_switch_notify) + 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.reloading_model = False + self.updating_row_edit_fields = False + self.dnd_autoscroll_timeout_id = 0 + + self.needs_saved = False + self.reload_model() + + self.update_treeview_state() + self.set_needs_saved(False) + + self.main_window.present_with_time(0) + + def reload_model(self, flat=False): + self.reloading_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) as e: + 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.reloading_model = 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 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 + actions = {} + + 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) + + actions[uuid] = (file, kf) + except GLib.Error as e: + print("Error loading action file '%s': %s" % (action_file, e.message)) + continue + + return actions + + def fill_model(self, model, parent, items, installed_actions): + disabled_actions = self.nemo_plugin_settings.get_strv("disabled-actions") + + for item in items: + row_type = item.get("type") + uuid = item.get('uuid') + position = item.get('position') + + 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, position, Row(item, kf, path, path.name not in disabled_actions)]) + + del installed_actions[uuid] + elif row_type == ROW_TYPE_SEPARATOR: + iter = model.append(parent, [new_hash(), "separator", ROW_TYPE_SEPARATOR, 0, Row(item, None, None, True)]) + else: + iter = model.append(parent, [new_hash(), uuid, row_type, position, 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, 0, Row(None, kf, path, enabled)]) + + 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 serialize_model(self, parent, model): + result = [] + + iter = model.iter_children(parent) + while iter: + row_type = model.get_value(iter, ROW_TYPE) + row = model.get_value(iter, ROW_OBJ) + + item = { + 'uuid': model.get_value(iter, ROW_UUID), + 'type': row_type, + 'position': model.get_value(iter, ROW_POSITION), + 'user-label': row.get_custom_label(), + 'user-icon': row.get_custom_icon() + } + + 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 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): + if self.reloading_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() + self.set_needs_saved(True) + + def on_treeview_position_changed(self, selection): + if self.reloading_model: + return + + self.update_row_controls() + + def update_row_controls(self): + row = self.get_selected_row_field(ROW_OBJ) + + if row is not None: + self.updating_row_edit_fields = True + + row_type = self.get_selected_row_field(ROW_TYPE) + row_uuid = self.get_selected_row_field(ROW_UUID) + + 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.action_enabled_switch.set_active(row.enabled) + self.action_enabled_switch.set_sensitive(row_type == ROW_TYPE_ACTION) + self.remove_submenu_button.set_sensitive(row_type in (ROW_TYPE_SUBMENU, ROW_TYPE_SEPARATOR)) + self.row_uuid_label.set_text(row_uuid if row_type == ROW_TYPE_ACTION else "") + + 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, "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 on_row_activated(self, path, column, data=None): + row_type = self.get_selected_row_field(ROW_TYPE) + if row_type != ROW_TYPE_ACTION: + return + + self.action_enabled_switch.set_active(not self.action_enabled_switch.get_active()) + + 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) + self.main_window.set_title("* " + _("Nemo Actions Layout Editor")) + else: + self.save_button.set_sensitive(False) + self.discard_changes_button.set_sensitive(False) + self.main_window.set_title(_("Nemo Actions Layout Editor")) + + self.needs_saved = needs_saved + + # Button signal handlers + + def open_actions_folder_clicked(self, button): + subprocess.Popen(["xdg-open", USER_ACTIONS_DIR]) + + def on_save_clicked(self, button): + self.save_model() + self.save_disabled_list() + self.set_needs_saved(False) + + def flatten_layout_clicked(self, button): + self.flatten_model() + + def on_discard_changes_clicked(self, button): + self.set_needs_saved(False) + self.reload_model() + self.update_treeview_state() + + 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, + 0, + Row({"uuid": "New Submenu"}, None, None, True)]) + + # new_path = self.model.get_path(new_iter) + # self.treeview.scroll_to_cell(new_path, None, True, 0.5, 0.5) + + 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, + 0, + 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 + + 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_source_row_by_hash(self.model, row_hash) + 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_action_enabled_switch_notify(self, switch, pspec): + if self.updating_row_edit_fields: + return + + row = self.get_selected_row_field(ROW_OBJ) + if row is not None: + row.enabled = switch.get_active() + self.selected_row_changed() + + # Cell render functions + + 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) + + # 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 = model.get_value(source_iter, ROW_OBJ) + 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.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, 0, row]) + else: + if position == Gtk.TreeViewDropPosition.BEFORE: + new_iter = self.model.insert_before(parent, target_iter, [new_hash(), source_uuid, source_type, 0, row]) + elif position == Gtk.TreeViewDropPosition.AFTER: + new_iter = self.model.insert_after(parent, target_iter, [new_hash(), source_uuid, source_type, 0, 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_source_row_by_hash(self.model, source_hash) + + self.update_positions(parent) + return 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, + 0, + 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_source_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 update_positions(self, parent): + if parent: + iter = self.model.iter_children(parent) + else: + iter = self.model.get_iter_first() + + position = 0 + while iter: + self.model.set_value(iter, ROW_POSITION, position) + position += 1 + if self.model.iter_has_child(iter): + self.update_positions(iter) + iter = self.model.iter_next(iter) + + def window_delete(self, window, event, data=None): + if not self.quit(): + return Gdk.EVENT_STOP + return Gdk.EVENT_PROPAGATE + + 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() + + self.main_window.destroy() + Gtk.main_quit() + return True + +if __name__ == "__main__": + import sys + + app = NemoActionsOrganizer() + Gtk.main() + + sys.exit(0) \ No newline at end of file diff --git a/debian/control b/debian/control index 5faa86539..e69374c52 100644 --- a/debian/control +++ b/debian/control @@ -21,6 +21,7 @@ Build-Depends: libgsf-1-dev, libgtk-3-dev (>= 3.10), libgtk-3-doc, + libjson-glib-dev (>= 1.6), libpango1.0-dev, libx11-dev, libxapp-dev (>= 2.0.0), diff --git a/gresources/nemo-blank-desktop-window-ui.xml b/gresources/nemo-blank-desktop-window-ui.xml new file mode 100644 index 000000000..b058eef3b --- /dev/null +++ b/gresources/nemo-blank-desktop-window-ui.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/gresources/nemo-places-sidebar-ui.xml b/gresources/nemo-places-sidebar-ui.xml new file mode 100644 index 000000000..8a768a1b5 --- /dev/null +++ b/gresources/nemo-places-sidebar-ui.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gresources/nemo-tree-sidebar-ui.xml b/gresources/nemo-tree-sidebar-ui.xml new file mode 100644 index 000000000..9069b6ae0 --- /dev/null +++ b/gresources/nemo-tree-sidebar-ui.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gresources/nemo.gresource.xml b/gresources/nemo.gresource.xml index 91d50e2c7..47f26cca3 100644 --- a/gresources/nemo.gresource.xml +++ b/gresources/nemo.gresource.xml @@ -7,13 +7,16 @@ nemo-desktop-preferences.glade nemo-search-bar.glade nemo-shortcuts.ui + nemo-blank-desktop-window-ui.xml nemo-icon-view-ui.xml nemo-directory-view-ui.xml nemo-desktop-icon-view-ui.xml nemo-desktop-icon-grid-view-ui.xml nemo-list-view-ui.xml + nemo-places-sidebar-ui.xml nemo-shell-ui.xml nemo-statusbar-ui.xml + nemo-tree-sidebar-ui.xml thumbnail_frame.png knob.png nemo-style-fallback.css diff --git a/libnemo-private/meson.build b/libnemo-private/meson.build index 151013ddb..b5d96e347 100644 --- a/libnemo-private/meson.build +++ b/libnemo-private/meson.build @@ -82,7 +82,7 @@ nemo_private_sources = [ ] nemo_private_deps = [ - cinnamon, eel, gail, gio_unix, glib, gmodule, gtk, libxml, math, nemo_extension, x11, xapp + cinnamon, eel, gail, gio_unix, glib, gmodule, gtk, json, libxml, math, nemo_extension, x11, xapp ] if libexif_enabled diff --git a/libnemo-private/nemo-action-manager.c b/libnemo-private/nemo-action-manager.c index 8aec397f7..86c62d81a 100644 --- a/libnemo-private/nemo-action-manager.c +++ b/libnemo-private/nemo-action-manager.c @@ -18,21 +18,36 @@ */ #include "nemo-action-manager.h" +#include "nemo-file.h" #include "nemo-directory.h" -#include "nemo-action.h" +#include "nemo-file-utilities.h" + #include +#include #define DEBUG_FLAG NEMO_DEBUG_ACTIONS #include -#include "nemo-file-utilities.h" +typedef struct { + JsonParser *json_parser; + GFileMonitor *config_dir_monitor; -G_DEFINE_TYPE (NemoActionManager, nemo_action_manager, G_TYPE_OBJECT); + GHashTable *actions_by_uuid; + GList *actions; + + GList *actions_directory_list; + gboolean action_list_dirty; +} NemoActionManagerPrivate; + +struct _NemoActionManager +{ + GObject parent_instance; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (NemoActionManager, nemo_action_manager, G_TYPE_OBJECT) static void refresh_actions (NemoActionManager *action_manager, NemoDirectory *directory); static void add_action_to_action_list (NemoActionManager *action_manager, NemoFile *file); -static gpointer parent_class; - enum { PROP_0, @@ -69,8 +84,11 @@ plugin_prefs_changed (GSettings *settings, gchar *key, gpointer user_data) { g_return_if_fail (NEMO_IS_ACTION_MANAGER (user_data)); + NemoActionManager *action_manager = NEMO_ACTION_MANAGER (user_data); + DEBUG ("Enabled actions changed, refreshing all."); - refresh_actions (NEMO_ACTION_MANAGER (user_data), NULL); + + refresh_actions (action_manager, NULL); } static void @@ -124,8 +142,9 @@ static void add_directory_to_actions_directory_list (NemoActionManager *action_manager, NemoDirectory *directory) { + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); add_directory_to_directory_list (action_manager, directory, - &action_manager->actions_directory_list, + &priv->actions_directory_list, G_CALLBACK (actions_changed)); } @@ -133,8 +152,9 @@ static void remove_directory_from_actions_directory_list (NemoActionManager *action_manager, NemoDirectory *directory) { + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); remove_directory_from_directory_list (action_manager, directory, - &action_manager->actions_directory_list, + &priv->actions_directory_list, G_CALLBACK (actions_changed)); } @@ -272,6 +292,8 @@ on_action_condition_changed (NemoActionManager *action_manager) static void add_action_to_action_list (NemoActionManager *action_manager, NemoFile *file) { + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + gchar *uri; gchar *action_name; NemoAction *action; @@ -296,12 +318,15 @@ add_action_to_action_list (NemoActionManager *action_manager, NemoFile *file) G_CALLBACK (on_action_condition_changed), action_manager); - action_manager->actions = g_list_append (action_manager->actions, action); + priv->actions = g_list_append (priv->actions, action); + g_hash_table_insert (priv->actions_by_uuid, action->uuid, action); } static void void_actions_for_directory (NemoActionManager *action_manager, NemoDirectory *directory) { + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + GFile *dir = nemo_directory_get_location (directory); const gchar *dir_path = g_file_peek_path (dir); GList *new_list = NULL; @@ -309,13 +334,14 @@ void_actions_for_directory (NemoActionManager *action_manager, NemoDirectory *di DEBUG ("Removing existing actions in %s:", dir_path); - for (l = action_manager->actions; l != NULL; l = l->next) { + for (l = priv->actions; l != NULL; l = l->next) { NemoAction *action = NEMO_ACTION (l->data); if (g_strcmp0 (dir_path, action->parent_dir) != 0) { new_list = g_list_prepend (new_list, g_object_ref (action)); } else { DEBUG ("Found %s", action->key_file_path); + g_hash_table_remove (priv->actions_by_uuid, action->uuid); } } @@ -323,51 +349,132 @@ void_actions_for_directory (NemoActionManager *action_manager, NemoDirectory *di g_object_unref (dir); - tmp = action_manager->actions; - action_manager->actions = new_list; + tmp = priv->actions; + priv->actions = new_list; g_list_free_full (tmp, g_object_unref); } +#define LAYOUT_FILENAME "actions-tree.json" + +static void +reload_actions_layout (NemoActionManager *action_manager) +{ + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + + GError *error = NULL; + g_autofree gchar *path = NULL; + + DEBUG ("Attempting to load action layout."); + + g_clear_object (&priv->json_parser); + + JsonParser *parser = json_parser_new (); + path = g_build_filename (g_get_user_config_dir (), "nemo", LAYOUT_FILENAME, NULL); + + if (!json_parser_load_from_file (parser, path, &error)) { + if (error != NULL) { + DEBUG ("JsonParser couldn't load file: %s\n", error->message); + if (error->code != G_FILE_ERROR_NOENT) { + g_critical ("Error loading action layout file: %s\n", error->message); + } + } + + g_clear_error (&error); + g_clear_object (&parser); + return; + } + + priv->json_parser = parser; + DEBUG ("Loaded action layout file: %s\n", path); +} + +static void +nemo_config_dir_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + NemoActionManager *action_manager = NEMO_ACTION_MANAGER (user_data); + + g_autofree gchar *basename = g_file_get_basename (file); + + if (g_strcmp0 (basename, LAYOUT_FILENAME) != 0) { + return; + } + + if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { + return; + } + + refresh_actions (action_manager, NULL); +} + +static void +monitor_nemo_config_dir (NemoActionManager *action_manager) +{ + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + GFile *file; + GFileMonitor *monitor; + + g_autofree gchar *path = g_build_filename (g_get_user_config_dir (), "nemo", NULL); + + file = g_file_new_for_path (path); + monitor = g_file_monitor_directory (file, G_FILE_MONITOR_WATCH_MOVES | G_FILE_MONITOR_SEND_MOVED, NULL, NULL); + g_object_unref (file); + g_signal_connect (monitor, "changed", G_CALLBACK (nemo_config_dir_changed), action_manager); + priv->config_dir_monitor = monitor; +} + static void refresh_actions (NemoActionManager *action_manager, NemoDirectory *directory) { - action_manager->action_list_dirty = TRUE; + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + + priv->action_list_dirty = TRUE; + + reload_actions_layout (action_manager); if (directory != NULL) { void_actions_for_directory (action_manager, directory); process_directory_actions (action_manager, directory); } else { - g_list_free_full (action_manager->actions, g_object_unref); - action_manager->actions = NULL; + g_list_free_full (priv->actions, g_object_unref); + priv->actions = NULL; + + g_hash_table_destroy (priv->actions_by_uuid); + priv->actions_by_uuid = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); GList *l; - for (l = action_manager->actions_directory_list; l != NULL; l = l->next) { + for (l = priv->actions_directory_list; l != NULL; l = l->next) { NemoDirectory *dir = NEMO_DIRECTORY (l->data); process_directory_actions (action_manager, dir); } } - action_manager->action_list_dirty = FALSE; - + priv->action_list_dirty = FALSE; g_signal_emit (action_manager, signals[CHANGED], 0); } static void nemo_action_manager_init (NemoActionManager *action_manager) { - action_manager->actions = NULL; - action_manager->actions_directory_list = NULL; } void nemo_action_manager_constructed (GObject *object) { - G_OBJECT_CLASS (parent_class)->constructed (object); + G_OBJECT_CLASS (nemo_action_manager_parent_class)->constructed (object); NemoActionManager *action_manager = NEMO_ACTION_MANAGER (object); + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + + priv->action_list_dirty = TRUE; + priv->actions_by_uuid = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); - action_manager->action_list_dirty = TRUE; set_up_actions_directories (action_manager); + reload_actions_layout (action_manager); + monitor_nemo_config_dir (action_manager); g_signal_connect (nemo_plugin_preferences, "changed::" NEMO_PLUGIN_PREFERENCES_DISABLED_ACTIONS, @@ -384,57 +491,81 @@ static void nemo_action_manager_dispose (GObject *object) { NemoActionManager *action_manager = NEMO_ACTION_MANAGER (object); + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); - if (action_manager->actions_directory_list != NULL) { + if (priv->actions_directory_list != NULL) { GList *node, *copy; - copy = nemo_directory_list_copy (action_manager->actions_directory_list); + copy = nemo_directory_list_copy (priv->actions_directory_list); for (node = copy; node != NULL; node = node->next) { remove_directory_from_actions_directory_list (action_manager, node->data); } - g_list_free (action_manager->actions_directory_list); - action_manager->actions_directory_list = NULL; + g_list_free (priv->actions_directory_list); + priv->actions_directory_list = NULL; nemo_directory_list_free (copy); } + g_clear_object (&priv->config_dir_monitor); + g_clear_object (&priv->json_parser); g_signal_handlers_disconnect_by_func (nemo_plugin_preferences, G_CALLBACK (plugin_prefs_changed), action_manager); - G_OBJECT_CLASS (parent_class)->dispose (object); + G_OBJECT_CLASS (nemo_action_manager_parent_class)->dispose (object); } static void nemo_action_manager_finalize (GObject *object) { NemoActionManager *action_manager = NEMO_ACTION_MANAGER (object); + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); - g_list_free_full (action_manager->actions, g_object_unref); + g_hash_table_destroy (priv->actions_by_uuid); + g_list_free_full (priv->actions, g_object_unref); - G_OBJECT_CLASS (parent_class)->finalize (object); + G_OBJECT_CLASS (nemo_action_manager_parent_class)->finalize (object); } static void nemo_action_manager_class_init (NemoActionManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); - parent_class = g_type_class_peek_parent (klass); object_class->finalize = nemo_action_manager_finalize; object_class->dispose = nemo_action_manager_dispose; object_class->constructed = nemo_action_manager_constructed; - signals[CHANGED] = - g_signal_new ("changed", - G_TYPE_FROM_CLASS (object_class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (NemoActionManagerClass, changed), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); + signals[CHANGED] = g_signal_new ("changed", + NEMO_TYPE_ACTION_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); } GList * nemo_action_manager_list_actions (NemoActionManager *action_manager) { - return action_manager->action_list_dirty ? NULL : action_manager->actions; + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + + return priv->action_list_dirty ? NULL : priv->actions; +} + +NemoAction * +nemo_action_manager_get_action (NemoActionManager *action_manager, + const gchar *uuid) +{ + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + NemoAction *action; + + if (priv->action_list_dirty || priv->actions == NULL) { + return NULL; + } + + action = g_hash_table_lookup (priv->actions_by_uuid, uuid); + if (action) { + return g_object_ref (action); + } + else { + return NULL; + } } gchar * @@ -461,3 +592,315 @@ nemo_action_manager_get_user_directory_path (void) { return g_build_filename (g_get_user_data_dir (), "nemo", "actions", NULL); } + +typedef struct +{ + NemoActionManager *action_manager; + JsonReader *reader; + + GError *error; + + NemoActionManagerIterFunc func; + gpointer user_data; +} ActionsIterData; + +static gboolean +parse_level (ActionsIterData *idata, + const gchar *path); + +static void +item_error (JsonReader *reader, + GError **error, + const gchar *uuid, + const gchar *custom_message) +{ + if (*error != NULL) { + return; + } + + const GError *internal_error = json_reader_get_error (reader); + gchar *error_message; + + if (internal_error != NULL) { + error_message = g_strdup_printf ("(%s) - %s: %s", uuid, custom_message, internal_error->message); + } else { + error_message = g_strdup_printf ("(%s) - %s", uuid, custom_message); + } + + *error = g_error_new_literal (json_parser_error_quark (), + JSON_PARSER_ERROR_INVALID_DATA, + error_message); + g_free (error_message); +} + +static gboolean +parse_item (ActionsIterData *idata, + const gchar *path) +{ + const gchar *type = NULL; + const gchar *uuid = NULL; + + JsonReader *reader = idata->reader; + + if (!json_reader_read_member (reader, "uuid") || json_reader_get_null_value (reader)) { + item_error (reader, &idata->error, path, "Action layout member is missing mandatory value"); + return FALSE; + } + uuid = json_reader_get_string_value (reader); + json_reader_end_member (reader); + + nemo_debug (NEMO_DEBUG_ACTIONS, "Parsing action layout entry for '%s'.", uuid); + + if (!json_reader_read_member (reader, "type") || json_reader_get_null_value (reader)) { + item_error (reader, &idata->error, uuid, "Action layout member is missing mandatory value"); + return FALSE; + } + type = json_reader_get_string_value (reader); + json_reader_end_member (reader); + + if (g_strcmp0 (type, "separator") == 0) { + nemo_debug (NEMO_DEBUG_ACTIONS, "Adding separator to UI."); + + idata->func (idata->action_manager, + NULL, + GTK_UI_MANAGER_SEPARATOR, + path, + idata->user_data); + return TRUE; + } + + const gchar *user_label = NULL; + const gchar *user_icon = NULL; + + // user-label and user-icon are optional, no error if they're missing or null. + if (json_reader_read_member (reader, "user-label") && !json_reader_get_null_value (reader)) { + user_label = json_reader_get_string_value (reader); + } + json_reader_end_member (reader); + + if (json_reader_read_member (reader, "user-icon") && !json_reader_get_null_value (reader)) { + user_icon = json_reader_get_string_value (reader); + } + json_reader_end_member (reader); + + if (g_strcmp0 (type, "action") == 0) { + NemoAction *action; + g_autofree gchar *lookup_uuid = nemo_make_action_uuid_for_path (uuid); + + action = nemo_action_manager_get_action (idata->action_manager, lookup_uuid); + + if (action == NULL) { + // Don't fail a bad action, we'll show a message and keep going. + DEBUG ("Missing action '%s' ignored in action layout", lookup_uuid); + return TRUE; + } + + if (user_label != NULL) { + nemo_action_override_label (action, user_label); + } + + if (user_icon != NULL) { + nemo_action_override_icon (action, user_icon); + } + + nemo_debug (NEMO_DEBUG_ACTIONS, "Adding action '%s' to UI.", action->uuid); + + idata->func (idata->action_manager, + GTK_ACTION (action), + GTK_UI_MANAGER_MENUITEM, + path, + idata->user_data); + g_object_unref (action); + + return TRUE; + } + else + if (g_strcmp0 (type, "submenu") == 0) { + GString *uuid_str; + g_autofree gchar *safe_uuid = NULL; + g_autofree gchar *next_path = NULL; + + // A submenu's UUID is just its label, which can have /'s. This messes with the action 'paths' being + // constructed when adding to the UI. + uuid_str = g_string_new (uuid); + g_string_replace (uuid_str, "/", "::", 0); + safe_uuid = g_string_free (uuid_str, FALSE); + + GtkAction *submenu = gtk_action_new (safe_uuid, user_label ? user_label : "", NULL, NULL); + + if (user_icon != NULL) { + gtk_action_set_icon_name (submenu, user_icon); + } + + // A submenu gets added to the same level (path) as its sibling menuitems + nemo_debug (NEMO_DEBUG_ACTIONS, "Adding submenu '%s' to UI.", uuid); + + idata->func (idata->action_manager, + submenu, + GTK_UI_MANAGER_MENU, + path, + idata->user_data); + + if (!json_reader_read_member (reader, "children") || json_reader_get_null_value (reader)) { + item_error (reader, &idata->error, uuid, "Layout submenu is missing mandatory 'children' field"); + return FALSE; + } + + // But its children will be added to the next level path (which is the old path + the menu uuid) + // Recursion happens, but 'next_path' becomes 'path' on the next level, and is never modified or + // freed... + if (path != NULL) { + next_path = g_strdup_printf ("%s/%s", path, safe_uuid); + } + else + { + next_path = g_strdup (safe_uuid); + } + + if (parse_level (idata, next_path)) { + json_reader_end_member (reader); + return TRUE; + } + // ...So next_path will be freed once return to this block after parse_level. + } + + return FALSE; +} + +static gboolean +parse_level (ActionsIterData *idata, + const gchar *path) +{ + JsonReader *reader = idata->reader; + + if (json_reader_is_array (reader)) { + guint len = json_reader_count_elements (reader); + nemo_debug (NEMO_DEBUG_ACTIONS, "Processing %d children of '%s'.", len, path == NULL ? "root" : path); + + gint i; + for (i = 0; i < len; i++) { + if (!json_reader_read_element (reader, i)) { + idata->error = g_error_copy (json_reader_get_error (reader)); + return FALSE; + } + + if (!parse_item (idata, path)) { + return FALSE; + } + + json_reader_end_element (reader); + } + + return TRUE; + } else { + idata->error = g_error_new (json_parser_error_quark (), + JSON_PARSER_ERROR_INVALID_DATA, + "Current layout depth lacks an array of action, submenu and separator definitions"); + } + + return FALSE; +} + +static gboolean +iter_actions (NemoActionManager *action_manager, + ActionsIterData *idata) +{ + JsonReader *reader = idata->reader; + + idata->error = NULL; + + if (json_reader_read_member (reader, "toplevel")) { + if (parse_level (idata, NULL)) { + json_reader_end_member (reader); + } + } + + if (idata->error == NULL && json_reader_get_error (reader)) { + idata->error = g_error_copy (json_reader_get_error (reader)); + } + + if (idata->error != NULL) { + g_critical ("Structured actions couldn't be set up: %p %s", idata->error, idata->error->message); + return FALSE; + } + + return TRUE; +} + +static JsonReader * +get_layout_reader (NemoActionManager *action_manager) +{ + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + + if (priv->action_list_dirty || priv->actions == NULL || priv->json_parser == NULL) { + return NULL; + } + + JsonReader *reader; + reader = json_reader_new (json_parser_get_root (priv->json_parser)); + + return reader; +} + +void +nemo_action_manager_iterate_actions (NemoActionManager *action_manager, + NemoActionManagerIterFunc func, + gpointer user_data) +{ + NemoActionManagerPrivate *priv = nemo_action_manager_get_instance_private (action_manager); + JsonReader *reader = get_layout_reader (action_manager); + gboolean ret = FALSE; + + if (reader != NULL) { + ActionsIterData idata; + + idata.action_manager = action_manager; + idata.reader = reader; + idata.func = func; + idata.user_data = user_data; + + ret = iter_actions (action_manager, &idata); + + g_object_unref (idata.reader); + } + + if (!ret) { + NemoAction *action; + GList *node; + + for (node = priv->actions; node != NULL; node = node->next) { + action = node->data; + + func (action_manager, + GTK_ACTION (action), + GTK_UI_MANAGER_MENUITEM, + NULL, + user_data); + } + } +} + +void +nemo_action_manager_update_action_states (NemoActionManager *action_manager, + GtkActionGroup *action_group, + GList *selection, + NemoFile *parent, + gboolean for_places, + GtkWindow *window) +{ + GList *l, *actions; + + actions = gtk_action_group_list_actions (action_group); + + for (l = actions; l != NULL; l = l->next) { + if (!NEMO_IS_ACTION (l->data)) { + nemo_debug (NEMO_DEBUG_ACTIONS, "Skipping submenu '%s' (visibility managed by GtkUIManager)", gtk_action_get_name (GTK_ACTION (l->data))); + gtk_action_set_visible (GTK_ACTION (l->data), TRUE); + continue; + } + + nemo_action_update_display_state (NEMO_ACTION (l->data), selection, parent, for_places, window); + } + + g_list_free (actions); +} diff --git a/libnemo-private/nemo-action-manager.h b/libnemo-private/nemo-action-manager.h index 35df6be38..1c2f9776f 100644 --- a/libnemo-private/nemo-action-manager.h +++ b/libnemo-private/nemo-action-manager.h @@ -21,39 +21,36 @@ #define NEMO_ACTION_MANAGER_H #include -#include "nemo-file.h" +#include + +#include "nemo-action.h" #define NEMO_TYPE_ACTION_MANAGER nemo_action_manager_get_type() -#define NEMO_ACTION_MANAGER(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST ((obj), NEMO_TYPE_ACTION_MANAGER, NemoActionManager)) -#define NEMO_ACTION_MANAGER_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST ((klass), NEMO_TYPE_ACTION_MANAGER, NemoActionManagerClass)) -#define NEMO_IS_ACTION_MANAGER(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NEMO_TYPE_ACTION_MANAGER)) -#define NEMO_IS_ACTION_MANAGER_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE ((klass), NEMO_TYPE_ACTION_MANAGER)) -#define NEMO_ACTION_MANAGER_GET_CLASS(obj) \ - (G_TYPE_INSTANCE_GET_CLASS ((obj), NEMO_TYPE_ACTION_MANAGER, NemoActionManagerClass)) - -typedef struct _NemoActionManager NemoActionManager; -typedef struct _NemoActionManagerClass NemoActionManagerClass; - -struct _NemoActionManager { - GObject parent; - GList *actions; - GList *actions_directory_list; - gboolean action_list_dirty; -}; - -struct _NemoActionManagerClass { - GObjectClass parent_class; - void (* changed) (NemoActionManager *action_manager); -}; - -GType nemo_action_manager_get_type (void); -NemoActionManager *nemo_action_manager_new (void); -GList * nemo_action_manager_list_actions (NemoActionManager *action_manager); -gchar * nemo_action_manager_get_system_directory_path (const gchar *data_dir); -gchar * nemo_action_manager_get_user_directory_path (void); + +G_DECLARE_FINAL_TYPE (NemoActionManager, nemo_action_manager, NEMO, ACTION_MANAGER, GObject) + +typedef void (* NemoActionManagerIterFunc) (NemoActionManager *manager, + GtkAction *action, + GtkUIManagerItemType type, + const gchar *path, + gpointer user_data); + +NemoActionManager * nemo_action_manager_new (void); +GList * nemo_action_manager_list_actions (NemoActionManager *action_manager); +JsonReader * nemo_action_manager_get_layout_reader (NemoActionManager *action_manager); +gchar * nemo_action_manager_get_system_directory_path (const gchar *data_dir); +gchar * nemo_action_manager_get_user_directory_path (void); +NemoAction * nemo_action_manager_get_action (NemoActionManager *action_manager, + const gchar *uuid); +void nemo_action_manager_iterate_actions (NemoActionManager *action_manager, + NemoActionManagerIterFunc callback, + gpointer user_data); +void nemo_action_manager_update_action_states (NemoActionManager *action_manager, + GtkActionGroup *action_group, + GList *selection, + NemoFile *parent, + gboolean for_places, + GtkWindow *window); #endif /* NEMO_ACTION_MANAGER_H */ + diff --git a/libnemo-private/nemo-action-symbols.h b/libnemo-private/nemo-action-symbols.h new file mode 100644 index 000000000..26fb22722 --- /dev/null +++ b/libnemo-private/nemo-action-symbols.h @@ -0,0 +1,101 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this program; if not, write to the + Free Software Foundation, Inc., 51 Franklin Street - Suite 500, + Boston, MA 02110-1335, USA. + +*/ + +#ifndef NEMO_ACTION_SYMBOLS_H +#define NEMO_ACTION_SYMBOLS_H + +/** + * List of uris + * */ +#define TOKEN_EXEC_URI_LIST "%U" + +/** + * List of paths + * */ +#define TOKEN_EXEC_FILE_LIST "%F" +#define TOKEN_EXEC_LOCATION_PATH "%P" // also parent path +#define TOKEN_EXEC_LOCATION_URI "%R" // and uri +#define TOKEN_EXEC_FILE_NAME "%f" +#define TOKEN_EXEC_PARENT_NAME "%p" +#define TOKEN_EXEC_DEVICE "%D" +#define TOKEN_EXEC_FILE_NO_EXT "%e" +#define TOKEN_EXEC_LITERAL_PERCENT "%%" +#define TOKEN_EXEC_XID "%X" + +#define TOKEN_LABEL_FILE_NAME "%N" // Leave in for compatibility, same as TOKEN_EXEC_FILE_NAME + + +#define SELECTION_SINGLE_KEY "s" +#define SELECTION_MULTIPLE_KEY "m" +#define SELECTION_ANY_KEY "any" +#define SELECTION_NONE_KEY "none" +#define SELECTION_NOT_NONE_KEY "notnone" + +typedef enum { + SELECTION_SINGLE = G_MAXINT - 10, + SELECTION_MULTIPLE, + SELECTION_NOT_NONE, + SELECTION_ANY, + SELECTION_NONE +} SelectionType; + +typedef enum { + QUOTE_TYPE_SINGLE = 0, + QUOTE_TYPE_DOUBLE, + QUOTE_TYPE_BACKTICK, + QUOTE_TYPE_NONE +} QuoteType; + +typedef enum { + TOKEN_NONE = 0, + TOKEN_PATH_LIST, + TOKEN_URI_LIST, + TOKEN_FILE_DISPLAY_NAME, + TOKEN_PARENT_DISPLAY_NAME, + TOKEN_PARENT_PATH, + TOKEN_PARENT_URI, + TOKEN_DEVICE, + TOKEN_FILE_DISPLAY_NAME_NO_EXT, + TOKEN_LITERAL_PERCENT, + TOKEN_XID +} TokenType; + +#define ACTION_FILE_GROUP "Nemo Action" + +#define KEY_ACTIVE "Active" +#define KEY_NAME "Name" +#define KEY_COMMENT "Comment" +#define KEY_EXEC "Exec" +#define KEY_ICON_NAME "Icon-Name" +#define KEY_STOCK_ID "Stock-Id" +#define KEY_SELECTION "Selection" +#define KEY_EXTENSIONS "Extensions" +#define KEY_MIME_TYPES "Mimetypes" +#define KEY_SEPARATOR "Separator" +#define KEY_QUOTE_TYPE "Quote" +#define KEY_DEPENDENCIES "Dependencies" +#define KEY_CONDITIONS "Conditions" +#define KEY_WHITESPACE "EscapeSpaces" +#define KEY_DOUBLE_ESCAPE_QUOTES "DoubleEscapeQuotes" +#define KEY_TERMINAL "Terminal" +#define KEY_URI_SCHEME "UriScheme" +#define KEY_FILES "Files" +#define KEY_LOCATIONS "Locations" + +#endif // NEMO_ACTION_SYMBOLS_H diff --git a/libnemo-private/nemo-action.c b/libnemo-private/nemo-action.c index 943ab96ea..205fdcc94 100644 --- a/libnemo-private/nemo-action.c +++ b/libnemo-private/nemo-action.c @@ -18,6 +18,7 @@ */ #include "nemo-action.h" +#include "nemo-action-symbols.h" #include #include #include @@ -25,6 +26,7 @@ #include #include "nemo-file-utilities.h" #include "nemo-program-choosing.h" +#include "nemo-ui-utilities.h" #define DEBUG_FLAG NEMO_DEBUG_ACTIONS #include @@ -33,8 +35,34 @@ #define g_drive_is_removable g_drive_is_media_removable #endif -G_DEFINE_TYPE (NemoAction, nemo_action, - GTK_TYPE_ACTION); +typedef struct { + SelectionType selection_type; + gchar **extensions; + gchar **mimetypes; + gchar *exec; + gchar **conditions; + gchar *separator; + QuoteType quote_type; + gchar *orig_label; + gchar *orig_tt; + gboolean use_parent_dir; + GList *dbus; + guint dbus_recalc_timeout_id; + GList *gsettings; + guint gsettings_recalc_timeout_id; + gboolean dbus_satisfied; + gboolean gsettings_satisfied; + gboolean escape_underscores; + gboolean escape_space; + gboolean show_in_blank_desktop; + gboolean run_in_terminal; + gchar *uri_scheme; + + + gboolean constructing; +} NemoActionPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (NemoAction, nemo_action, GTK_TYPE_ACTION) static void nemo_action_get_property (GObject *object, guint param_id, @@ -49,9 +77,7 @@ static void nemo_action_finalize (GObject *gobject); static gchar *find_token_type (const gchar *str, TokenType *token_type); -static gpointer parent_class; - -enum +enum { PROP_0, PROP_KEY_FILE_PATH, @@ -121,38 +147,25 @@ gsettings_condition_free (gpointer data) static void nemo_action_init (NemoAction *action) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + action->key_file_path = NULL; - action->selection_type = SELECTION_SINGLE; - action->extensions = NULL; - action->mimetypes = NULL; - action->exec = NULL; action->parent_dir = NULL; - action->use_parent_dir = FALSE; - action->orig_label = NULL; - action->orig_tt = NULL; - action->quote_type = QUOTE_TYPE_NONE; - action->separator = NULL; - action->conditions = NULL; - action->dbus = NULL; - action->dbus_satisfied = TRUE; - action->dbus_recalc_timeout_id = 0; - action->gsettings = NULL; - action->gsettings_satisfied = TRUE; - action->gsettings_recalc_timeout_id = 0; - action->escape_underscores = FALSE; - action->escape_space = FALSE; - action->show_in_blank_desktop = FALSE; - action->run_in_terminal = FALSE; - action->uri_scheme = NULL; - - action->constructing = TRUE; + action->uuid = NULL; + + priv->selection_type = SELECTION_SINGLE; + priv->quote_type = QUOTE_TYPE_NONE; + priv->dbus_satisfied = TRUE; + priv->dbus_recalc_timeout_id = 0; + priv->gsettings_satisfied = TRUE; + priv->gsettings_recalc_timeout_id = 0; + priv->constructing = TRUE; } static void nemo_action_class_init (NemoActionClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); - parent_class = g_type_class_peek_parent (klass); object_class->finalize = nemo_action_finalize; object_class->set_property = nemo_action_set_property; object_class->get_property = nemo_action_get_property; @@ -167,128 +180,6 @@ nemo_action_class_init (NemoActionClass *klass) G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY) ); - g_object_class_install_property (object_class, - PROP_SELECTION_TYPE, - g_param_spec_int ("selection-type", - "Selection Type", - "The action selection type", - 0, - SELECTION_NONE, - SELECTION_SINGLE, - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_EXTENSIONS, - g_param_spec_pointer ("extensions", - "Extensions", - "String array of file extensions", - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_MIMES, - g_param_spec_pointer ("mimetypes", - "Mimetypes", - "String array of file mimetypes", - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_EXEC, - g_param_spec_string ("exec", - "Executable String", - "The command line to run", - NULL, - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_PARENT_DIR, - g_param_spec_string ("parent-dir", - "Parent directory", - "The directory the action file resides in", - NULL, - G_PARAM_READWRITE) - ); - g_object_class_install_property (object_class, - PROP_USE_PARENT_DIR, - g_param_spec_boolean ("use-parent-dir", - "Use Parent Directory", - "Execute using the full action path", - FALSE, - G_PARAM_READWRITE) - ); - g_object_class_install_property (object_class, - PROP_ORIG_LABEL, - g_param_spec_string ("orig-label", - "Original label string", - "The starting label - with token", - NULL, - G_PARAM_READWRITE) - ); - g_object_class_install_property (object_class, - PROP_ORIG_TT, - g_param_spec_string ("orig-tooltip", - "Original tooltip string", - "The starting tooltip - with token", - NULL, - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_SEPARATOR, - g_param_spec_string ("separator", - "Separator to insert between files in the exec line", - "Separator to use between files, like comma, space, etc", - NULL, - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_QUOTE_TYPE, - g_param_spec_int ("quote-type", - "Type of quotes to use to enclose individual file names", - "Type of quotes to use to enclose individual file names - none, single or double", - QUOTE_TYPE_SINGLE, - QUOTE_TYPE_NONE, - QUOTE_TYPE_SINGLE, - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_CONDITIONS, - g_param_spec_pointer ("conditions", - "Special show conditions", - "Special conditions, like a bool gsettings key, or 'desktop'", - G_PARAM_READWRITE) - ); - g_object_class_install_property (object_class, - PROP_ESCAPE_SPACE, - g_param_spec_boolean ("escape-space", - "Escape spaces in file paths", - "Escape spaces in file paths", - FALSE, - G_PARAM_READWRITE) - ); - g_object_class_install_property (object_class, - PROP_RUN_IN_TERMINAL, - g_param_spec_boolean ("run-in-terminal", - "Run command in a terminal", - "Run command in a terminal", - FALSE, - G_PARAM_READWRITE) - ); - - g_object_class_install_property (object_class, - PROP_URI_SCHEME, - g_param_spec_string ("uri-scheme", - "Limit selection by uri scheme (like file, sftp, etc...)", - "Limit selection by uri scheme (like file, sftp, etc...)", - NULL, - G_PARAM_READWRITE) - ); - signals[CONDITION_CHANGED] = g_signal_new ("condition-changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, @@ -300,6 +191,8 @@ nemo_action_class_init (NemoActionClass *klass) static gboolean recalc_dbus_conditions (NemoAction *action) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + GList *l; DBusCondition *c; gboolean pass, old_satisfied; @@ -308,7 +201,7 @@ recalc_dbus_conditions (NemoAction *action) pass = TRUE; - for (l = action->dbus; l != NULL; l = l->next) { + for (l = priv->dbus; l != NULL; l = l->next) { c = (DBusCondition *) l->data; DEBUG ("Checking dbus name for an owner: '%s' - evaluated to %s", @@ -321,8 +214,8 @@ recalc_dbus_conditions (NemoAction *action) } } - old_satisfied = action->dbus_satisfied; - action->dbus_satisfied = pass; + old_satisfied = priv->dbus_satisfied; + priv->dbus_satisfied = pass; DEBUG ("DBus satisfied: %s", pass ? "TRUE" : "FALSE"); @@ -331,22 +224,24 @@ recalc_dbus_conditions (NemoAction *action) g_signal_emit (action, signals[CONDITION_CHANGED], 0); } - action->dbus_recalc_timeout_id = 0; + priv->dbus_recalc_timeout_id = 0; return FALSE; } static void queue_recalc_dbus_conditions (NemoAction *action) { - if (action->constructing) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + + if (priv->constructing) { return; } - if (action->dbus_recalc_timeout_id != 0) { - g_source_remove (action->dbus_recalc_timeout_id); - action->dbus_recalc_timeout_id = 0; + if (priv->dbus_recalc_timeout_id != 0) { + g_source_remove (priv->dbus_recalc_timeout_id); + priv->dbus_recalc_timeout_id = 0; } - action->dbus_recalc_timeout_id = g_idle_add ((GSourceFunc) recalc_dbus_conditions, + priv->dbus_recalc_timeout_id = g_idle_add ((GSourceFunc) recalc_dbus_conditions, action); } @@ -376,6 +271,8 @@ on_dbus_disappeared (GDBusConnection *connection, static void setup_dbus_condition (NemoAction *action, const gchar *condition) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + gchar **split = g_strsplit (condition, " ", 2); if (g_strv_length (split) != 2) { @@ -393,7 +290,7 @@ setup_dbus_condition (NemoAction *action, const gchar *condition) cond->name = g_strdup (split[1]); cond->exists = FALSE; cond->action = action; - action->dbus = g_list_append (action->dbus, cond); + priv->dbus = g_list_append (priv->dbus, cond); cond->watch_id = g_bus_watch_name (G_BUS_TYPE_SESSION, cond->name, 0, @@ -446,6 +343,8 @@ try_vector (const gchar *op, gint vector) static gboolean recalc_gsettings_conditions (NemoAction *action) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + GList *l; gboolean pass, old_satisfied; @@ -453,7 +352,7 @@ recalc_gsettings_conditions (NemoAction *action) pass = TRUE; - for (l = action->gsettings; l != NULL; l = l->next) { + for (l = priv->gsettings; l != NULL; l = l->next) { GSettingsCondition *cond; const GVariantType *target_type, *setting_type; gchar **split; @@ -525,30 +424,32 @@ recalc_gsettings_conditions (NemoAction *action) DEBUG ("GSettings satisfied: %s", pass ? "TRUE" : "FALSE"); - old_satisfied = action->gsettings_satisfied; - action->gsettings_satisfied = pass; + old_satisfied = priv->gsettings_satisfied; + priv->gsettings_satisfied = pass; if (pass != old_satisfied) { g_signal_emit (action, signals[CONDITION_CHANGED], 0); } - action->gsettings_recalc_timeout_id = 0; + priv->gsettings_recalc_timeout_id = 0; return FALSE; } static void queue_recalc_gsettings_conditions (NemoAction *action) { - if (action->constructing) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + + if (priv->constructing) { return; } - if (action->gsettings_recalc_timeout_id != 0) { - g_source_remove (action->gsettings_recalc_timeout_id); - action->gsettings_recalc_timeout_id = 0; + if (priv->gsettings_recalc_timeout_id != 0) { + g_source_remove (priv->gsettings_recalc_timeout_id); + priv->gsettings_recalc_timeout_id = 0; } - action->gsettings_recalc_timeout_id = g_idle_add ((GSourceFunc) recalc_gsettings_conditions, + priv->gsettings_recalc_timeout_id = g_idle_add ((GSourceFunc) recalc_gsettings_conditions, action); } @@ -556,6 +457,8 @@ static void setup_gsettings_condition (NemoAction *action, const gchar *condition) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + GSettingsSchemaSource *schema_source; GSettingsSchema *schema; gchar **split; @@ -614,7 +517,7 @@ setup_gsettings_condition (NemoAction *action, G_CALLBACK (queue_recalc_gsettings_conditions), action); - action->gsettings = g_list_prepend (action->gsettings, cond); + priv->gsettings = g_list_prepend (priv->gsettings, cond); g_free (signal_string); @@ -647,9 +550,10 @@ strip_custom_modifier (const gchar *raw, gboolean *custom, gchar **out) void nemo_action_constructed (GObject *object) { - G_OBJECT_CLASS (parent_class)->constructed (object); - NemoAction *action = NEMO_ACTION (object); + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + + G_OBJECT_CLASS (nemo_action_parent_class)->constructed (object); GKeyFile *key_file = g_key_file_new(); @@ -816,7 +720,7 @@ nemo_action_constructed (GObject *object) TokenType token_type; - action->show_in_blank_desktop = is_desktop && + priv->show_in_blank_desktop = is_desktop && type == SELECTION_NONE && find_token_type (exec, &token_type) == NULL; @@ -833,41 +737,33 @@ nemo_action_constructed (GObject *object) "tooltip", orig_tt, "icon-name", icon_name, "stock-id", stock_id, - "exec", exec, - "selection-type", type, - "extensions", ext, - "mimetypes", mimes, - "parent-dir", parent_dir, - "use-parent-dir", use_parent_dir, - "orig-label", orig_label, - "orig-tooltip", orig_tt, - "quote-type", quote_type, - "separator", separator, - "conditions", conditions, - "escape-space", escape_space, - "run-in-terminal", run_in_terminal, - "uri-scheme", uri_scheme, NULL); - action->constructing = FALSE; + priv->orig_label = orig_label; + priv->orig_tt = orig_tt; + priv->exec = exec; + priv->selection_type = type; + priv->extensions = ext; + priv->mimetypes = mimes; + action->parent_dir = parent_dir; + priv->use_parent_dir = use_parent_dir; + priv->quote_type = quote_type; + priv->separator = separator; + priv->conditions = conditions; + priv->escape_space = escape_space; + priv->run_in_terminal = run_in_terminal; + priv->uri_scheme = uri_scheme; + + priv->constructing = FALSE; DEBUG ("Initial action gsettings and dbus update (%s)", action->key_file_path); queue_recalc_dbus_conditions (action); queue_recalc_gsettings_conditions (action); DEBUG ("Initial action gsettings and dbus complete (%s)", action->key_file_path); - g_free (orig_label); - g_free (orig_tt); g_free (icon_name); g_free (stock_id); - g_free (exec); - g_free (parent_dir); g_free (quote_type_string); - g_free (separator); - g_free (uri_scheme); - g_strfreev (ext); - g_strfreev (mimes); - g_strfreev (conditions); g_key_file_free (key_file); } @@ -954,13 +850,15 @@ nemo_action_new (const gchar *name, if (reverse) { if (found) { finish = FALSE; - DEBUG ("Missing action reverse dependency: %s", deps[i]); + g_autofree gchar *base = g_path_get_basename (path); + g_warning_once ("Action '%s' is missing reverse dependency: %s", base, deps[i]); break; } } else { if (!found) { finish = FALSE; - DEBUG ("Missing action dependency: %s", deps[i]); + g_autofree gchar *base = g_path_get_basename (path); + g_warning_once ("Action '%s' is missing dependency: %s", base, deps[i]); break; } } @@ -992,98 +890,45 @@ static void nemo_action_finalize (GObject *object) { NemoAction *action = NEMO_ACTION (object); + NemoActionPrivate *priv = nemo_action_get_instance_private (action); g_free (action->key_file_path); - g_strfreev (action->extensions); - g_strfreev (action->mimetypes); - g_strfreev (action->conditions); - g_free (action->exec); + g_strfreev (priv->extensions); + g_strfreev (priv->mimetypes); + g_strfreev (priv->conditions); + g_free (priv->exec); g_free (action->parent_dir); - g_free (action->orig_label); - g_free (action->orig_tt); - g_free (action->separator); - g_free (action->uri_scheme); - - if (action->dbus) { - g_list_free_full (action->dbus, (GDestroyNotify) dbus_condition_free); - action->dbus = NULL; - } + g_free (priv->orig_label); + g_free (priv->orig_tt); + g_free (priv->separator); + g_free (priv->uri_scheme); + g_free (action->uuid); - if (action->gsettings) { - g_list_free_full (action->gsettings, (GDestroyNotify) gsettings_condition_free); - action->gsettings = NULL; - } + g_list_free_full (priv->dbus, (GDestroyNotify) dbus_condition_free); + g_list_free_full (priv->gsettings, (GDestroyNotify) gsettings_condition_free); - if (action->dbus_recalc_timeout_id != 0) { - g_source_remove (action->dbus_recalc_timeout_id); - action->dbus_recalc_timeout_id = 0; - } - if (action->gsettings_recalc_timeout_id != 0) { - g_source_remove (action->gsettings_recalc_timeout_id); - action->gsettings_recalc_timeout_id = 0; - } + g_clear_handle_id (&priv->dbus_recalc_timeout_id, g_source_remove); + g_clear_handle_id (&priv->gsettings_recalc_timeout_id, g_source_remove); - G_OBJECT_CLASS (parent_class)->finalize (object); + G_OBJECT_CLASS (nemo_action_parent_class)->finalize (object); } - static void nemo_action_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { - NemoAction *action; + NemoAction *action; - action = NEMO_ACTION (object); + action = NEMO_ACTION (object); - switch (prop_id) + switch (prop_id) { case PROP_KEY_FILE_PATH: action->key_file_path = g_strdup (g_value_get_string (value)); - break; - case PROP_SELECTION_TYPE: - action->selection_type = g_value_get_int (value); - break; - case PROP_EXTENSIONS: - action->extensions = g_strdupv (g_value_get_pointer (value)); - break; - case PROP_MIMES: - action->mimetypes = g_strdupv (g_value_get_pointer (value)); - break; - case PROP_EXEC: - action->exec = g_strdup (g_value_get_string (value)); - break; - case PROP_PARENT_DIR: - action->parent_dir = g_strdup (g_value_get_string (value)); - break; - case PROP_USE_PARENT_DIR: - action->use_parent_dir = g_value_get_boolean (value); - break; - case PROP_ORIG_LABEL: - action->orig_label = g_strdup (g_value_get_string (value)); - break; - case PROP_ORIG_TT: - action->orig_tt = g_strdup (g_value_get_string (value)); - break; - case PROP_QUOTE_TYPE: - action->quote_type = g_value_get_int (value); - break; - case PROP_SEPARATOR: - action->separator = g_strdup (g_value_get_string (value)); - break; - case PROP_CONDITIONS: - action->conditions = g_strdupv (g_value_get_pointer (value)); - break; - case PROP_ESCAPE_SPACE: - action->escape_space = g_value_get_boolean (value); - break; - case PROP_RUN_IN_TERMINAL: - action->run_in_terminal = g_value_get_boolean (value); - break; - case PROP_URI_SCHEME: - action->uri_scheme = g_strdup (g_value_get_string (value)); + action->uuid = nemo_make_action_uuid_for_path (action->key_file_path); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -1097,57 +942,15 @@ nemo_action_get_property (GObject *object, GValue *value, GParamSpec *pspec) { - NemoAction *action; + NemoAction *action; - action = NEMO_ACTION (object); + action = NEMO_ACTION (object); - switch (prop_id) + switch (prop_id) { case PROP_KEY_FILE_PATH: g_value_set_string (value, action->key_file_path); break; - case PROP_SELECTION_TYPE: - g_value_set_int (value, action->selection_type); - break; - case PROP_EXTENSIONS: - g_value_set_pointer (value, action->extensions); - break; - case PROP_MIMES: - g_value_set_pointer (value, action->mimetypes); - break; - case PROP_EXEC: - g_value_set_string (value, action->exec); - break; - case PROP_PARENT_DIR: - g_value_set_string (value, action->parent_dir); - break; - case PROP_USE_PARENT_DIR: - g_value_set_boolean (value, action->use_parent_dir); - break; - case PROP_ORIG_LABEL: - g_value_set_string (value, action->orig_label); - break; - case PROP_ORIG_TT: - g_value_set_string (value, action->orig_tt); - break; - case PROP_QUOTE_TYPE: - g_value_set_int (value, action->quote_type); - break; - case PROP_SEPARATOR: - g_value_set_string (value, action->separator); - break; - case PROP_CONDITIONS: - g_value_set_pointer (value, action->conditions); - break; - case PROP_ESCAPE_SPACE: - g_value_set_boolean (value, action->escape_space); - break; - case PROP_RUN_IN_TERMINAL: - g_value_set_boolean (value, action->run_in_terminal); - break; - case PROP_URI_SCHEME: - g_value_set_string (value, action->uri_scheme); - break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -1215,13 +1018,14 @@ find_token_type (const gchar *str, TokenType *token_type) static gchar * get_path (NemoAction *action, NemoFile *file) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); gchar *ret, *orig; orig = nemo_file_get_path (file); - if (action->quote_type == QUOTE_TYPE_DOUBLE) { + if (priv->quote_type == QUOTE_TYPE_DOUBLE) { ret = eel_str_escape_double_quoted_content (orig); - } else if (action->quote_type == QUOTE_TYPE_SINGLE) { + } else if (priv->quote_type == QUOTE_TYPE_SINGLE) { // Replace literal ' with a close ', a \', and an open ' ret = eel_str_replace_substring (orig, "'", "'\\''"); } else { @@ -1236,7 +1040,9 @@ get_path (NemoAction *action, NemoFile *file) static GString * score_append (NemoAction *action, GString *str, const gchar *c) { - if (action->escape_underscores) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + + if (priv->escape_underscores) { gchar *escaped = eel_str_double_underscores (c); str = g_string_append (str, escaped); g_free (escaped); @@ -1249,10 +1055,12 @@ score_append (NemoAction *action, GString *str, const gchar *c) static GString * insert_separator (NemoAction *action, GString *str) { - if (action->separator == NULL) + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + + if (priv->separator == NULL) str = g_string_append (str, " "); else - str = score_append (action, str, action->separator); + str = score_append (action, str, priv->separator); return str; } @@ -1260,7 +1068,9 @@ insert_separator (NemoAction *action, GString *str) static GString * insert_quote (NemoAction *action, GString *str) { - switch (action->quote_type) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + + switch (priv->quote_type) { case QUOTE_TYPE_SINGLE: str = g_string_append (str, "'"); break; @@ -1282,6 +1092,7 @@ insert_quote (NemoAction *action, GString *str) static gchar * get_device_path (NemoAction *action, NemoFile *file) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); GMount *mount = nemo_file_get_mount (file); g_return_val_if_fail (mount != NULL, NULL); @@ -1291,9 +1102,9 @@ get_device_path (NemoAction *action, NemoFile *file) id = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE); - if (action->quote_type == QUOTE_TYPE_DOUBLE) { + if (priv->quote_type == QUOTE_TYPE_DOUBLE) { ret = eel_str_escape_double_quoted_content (id); - } else if (action->quote_type == QUOTE_TYPE_SINGLE) { + } else if (priv->quote_type == QUOTE_TYPE_SINGLE) { // Replace literal ' with a close ', a \', and an open ' ret = eel_str_replace_substring (id, "'", "'\\''"); } else { @@ -1509,23 +1320,24 @@ nemo_action_activate (NemoAction *action, NemoFile *parent, GtkWindow *window) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); GError *error; - GString *exec = g_string_new (action->exec); + GString *exec = g_string_new (priv->exec); error = NULL; - action->escape_underscores = FALSE; + priv->escape_underscores = FALSE; exec = expand_action_string (action, selection, parent, exec, window); - if (action->use_parent_dir) { + if (priv->use_parent_dir) { exec = g_string_prepend (exec, G_DIR_SEPARATOR_S); exec = g_string_prepend (exec, action->parent_dir); } DEBUG ("Action Spawning: %s", exec->str); - if (action->run_in_terminal) { + if (priv->run_in_terminal) { gint argcp; gchar **argvp; @@ -1554,28 +1366,46 @@ nemo_action_activate (NemoAction *action, const gchar * nemo_action_get_orig_label (NemoAction *action) { - return action->orig_label; + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + return priv->orig_label; +} + +void +nemo_action_override_label (NemoAction *action, + const gchar *label) +{ + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + g_free (priv->orig_label); + priv->orig_label = g_strdup (label); +} + +void +nemo_action_override_icon (NemoAction *action, + const gchar *icon) +{ + gtk_action_set_icon_name (GTK_ACTION (action), icon); } const gchar * nemo_action_get_orig_tt (NemoAction *action) { - return action->orig_tt; + NemoActionPrivate *priv = nemo_action_get_instance_private (action); + return priv->orig_tt; } - -gchar * -nemo_action_get_label (NemoAction *action, - GList *selection, - NemoFile *parent, - GtkWindow *window) +static gchar * +get_final_label (NemoAction *action, + GList *selection, + NemoFile *parent, + GtkWindow *window) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); const gchar *orig_label = nemo_action_get_orig_label (action); if (orig_label == NULL) return NULL; - action->escape_underscores = TRUE; + priv->escape_underscores = TRUE; GString *str = g_string_new (orig_label); @@ -1588,18 +1418,19 @@ nemo_action_get_label (NemoAction *action, return ret; } -gchar * -nemo_action_get_tt (NemoAction *action, - GList *selection, - NemoFile *parent, - GtkWindow *window) +static gchar * +get_final_tt (NemoAction *action, + GList *selection, + NemoFile *parent, + GtkWindow *window) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); const gchar *orig_tt = nemo_action_get_orig_tt (action); if (orig_tt == NULL) return NULL; - action->escape_underscores = FALSE; + priv->escape_underscores = FALSE; GString *str = g_string_new (orig_tt); @@ -1612,6 +1443,24 @@ nemo_action_get_tt (NemoAction *action, return ret; } +static void +finalize_strings (NemoAction *action, + GList *selection, + NemoFile *parent, + GtkWindow *window) +{ + gchar *label, *tt; + + label = get_final_label (action, selection, parent, window); + tt = get_final_tt (action, selection, parent, window); + + gtk_action_set_label (GTK_ACTION (action), label); + gtk_action_set_tooltip (GTK_ACTION (action), tt); + + g_free (label); + g_free (tt); +} + static gboolean check_exec_condition (NemoAction *action, const gchar *condition, @@ -1619,6 +1468,7 @@ check_exec_condition (NemoAction *action, NemoFile *parent, GtkWindow *window) { + NemoActionPrivate *priv = nemo_action_get_instance_private (action); GString *exec; GError *error; gint return_code; @@ -1648,7 +1498,7 @@ check_exec_condition (NemoAction *action, error = NULL; - action->escape_underscores = FALSE; + priv->escape_underscores = FALSE; exec = expand_action_string (action, selection, parent, exec, window); @@ -1698,21 +1548,26 @@ get_is_dir (NemoFile *file) return ret; } -gboolean -nemo_action_get_visibility (NemoAction *action, - GList *selection, - NemoFile *parent, - gboolean for_places, - GtkWindow *window) { +static gboolean +get_visibility (NemoAction *action, + GList *selection, + NemoFile *parent, + gboolean for_places, + GtkWindow *window) +{ + NemoActionPrivate *priv = nemo_action_get_instance_private (action); // Check DBUS - if (!action->dbus_satisfied) + if (!priv->dbus_satisfied) + return FALSE; + + if (!priv->gsettings_satisfied) return FALSE; - if (!action->gsettings_satisfied) + if ((priv->uri_scheme != NULL) && !nemo_file_has_uri_scheme (parent, priv->uri_scheme)) { return FALSE; + } - if ((action->uri_scheme != NULL) && !nemo_file_has_uri_scheme (parent, action->uri_scheme)) { return FALSE; } @@ -1720,7 +1575,7 @@ nemo_action_get_visibility (NemoAction *action, gboolean selection_type_show = FALSE; guint selected_count = g_list_length (selection); - switch (action->selection_type) { + switch (priv->selection_type) { case SELECTION_SINGLE: selection_type_show = selected_count == 1; break; @@ -1737,7 +1592,7 @@ nemo_action_get_visibility (NemoAction *action, selection_type_show = TRUE; break; default: - selection_type_show = selected_count == action->selection_type; + selection_type_show = selected_count == priv->selection_type; break; } @@ -1746,8 +1601,8 @@ nemo_action_get_visibility (NemoAction *action, // Check extensions and mimetypes gboolean extension_type_show = TRUE; - gchar **extensions = action->extensions; - gchar **mimetypes = action->mimetypes; + gchar **extensions = priv->extensions; + gchar **mimetypes = priv->mimetypes; guint ext_count = extensions != NULL ? g_strv_length (extensions) : 0; guint mime_count = mimetypes != NULL ? g_strv_length (mimetypes) : 0; @@ -1822,7 +1677,7 @@ nemo_action_get_visibility (NemoAction *action, // Check conditions gboolean condition_type_show = TRUE; - gchar **conditions = action->conditions; + gchar **conditions = priv->conditions; guint condition_count = conditions != NULL ? g_strv_length (conditions) : 0; if (condition_count > 0) { @@ -1832,7 +1687,7 @@ nemo_action_get_visibility (NemoAction *action, condition = conditions[j]; if (g_strcmp0 (condition, "desktop") == 0) { gchar *name = nemo_file_get_display_name (parent); - if (g_strcmp0 (name, "x-nemo-desktop") != 0) + if (g_strcmp0 (name, "x-nemo-desktop") != 0 && !priv->show_in_blank_desktop) condition_type_show = FALSE; g_free (name); } else if (g_strcmp0 (condition, "removable") == 0) { @@ -1889,8 +1744,29 @@ nemo_action_get_visibility (NemoAction *action, } } - if (!condition_type_show) + if (!condition_type_show) { return FALSE; + } return TRUE; } + +void +nemo_action_update_display_state (NemoAction *action, + GList *selection, + NemoFile *parent, + gboolean for_places, + GtkWindow *window) +{ + if (get_visibility (action, selection, parent, for_places, window)) { + nemo_debug (NEMO_DEBUG_ACTIONS, "Action '%s' determined VISIBLE", gtk_action_get_name (GTK_ACTION (action))); + + finalize_strings (action, selection, parent, window); + gtk_action_set_visible (GTK_ACTION (action), TRUE); + + return; + } + + nemo_debug (NEMO_DEBUG_ACTIONS, "Action '%s' determined HIDDEN", gtk_action_get_name (GTK_ACTION (action))); + gtk_action_set_visible (GTK_ACTION (action), FALSE); +} diff --git a/libnemo-private/nemo-action.h b/libnemo-private/nemo-action.h index fbb60ff83..f73fb78d7 100644 --- a/libnemo-private/nemo-action.h +++ b/libnemo-private/nemo-action.h @@ -24,125 +24,24 @@ #include #include "nemo-file.h" -#define NEMO_TYPE_ACTION nemo_action_get_type() -#define NEMO_ACTION(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST ((obj), NEMO_TYPE_ACTION, NemoAction)) -#define NEMO_ACTION_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST ((klass), NEMO_TYPE_ACTION, NemoActionClass)) -#define NEMO_IS_ACTION(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NEMO_TYPE_ACTION)) -#define NEMO_IS_ACTION_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE ((klass), NEMO_TYPE_ACTION)) -#define NEMO_ACTION_GET_CLASS(obj) \ - (G_TYPE_INSTANCE_GET_CLASS ((obj), NEMO_TYPE_ACTION, NemoActionClass)) - - -#define SELECTION_SINGLE_KEY "s" -#define SELECTION_MULTIPLE_KEY "m" -#define SELECTION_ANY_KEY "any" -#define SELECTION_NONE_KEY "none" -#define SELECTION_NOT_NONE_KEY "notnone" - -#define TOKEN_EXEC_URI_LIST "%U" -#define TOKEN_EXEC_FILE_LIST "%F" -#define TOKEN_EXEC_LOCATION_PATH "%P" // also parent path -#define TOKEN_EXEC_LOCATION_URI "%R" // and uri -#define TOKEN_EXEC_FILE_NAME "%f" -#define TOKEN_EXEC_PARENT_NAME "%p" -#define TOKEN_EXEC_DEVICE "%D" -#define TOKEN_EXEC_FILE_NO_EXT "%e" -#define TOKEN_EXEC_LITERAL_PERCENT "%%" -#define TOKEN_EXEC_XID "%X" - -#define TOKEN_LABEL_FILE_NAME "%N" // Leave in for compatibility, same as TOKEN_EXEC_FILE_NAME - - -#define ACTION_FILE_GROUP "Nemo Action" +// GtkAction were deprecated before auto-free functionality was added. +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GtkAction, g_object_unref) -#define KEY_ACTIVE "Active" -#define KEY_NAME "Name" -#define KEY_COMMENT "Comment" -#define KEY_EXEC "Exec" -#define KEY_ICON_NAME "Icon-Name" -#define KEY_STOCK_ID "Stock-Id" -#define KEY_SELECTION "Selection" -#define KEY_EXTENSIONS "Extensions" -#define KEY_MIME_TYPES "Mimetypes" -#define KEY_SEPARATOR "Separator" -#define KEY_QUOTE_TYPE "Quote" -#define KEY_DEPENDENCIES "Dependencies" -#define KEY_CONDITIONS "Conditions" -#define KEY_WHITESPACE "EscapeSpaces" -#define KEY_DOUBLE_ESCAPE_QUOTES "DoubleEscapeQuotes" -#define KEY_TERMINAL "Terminal" -#define KEY_URI_SCHEME "UriScheme" - -typedef struct _NemoAction NemoAction; -typedef struct _NemoActionClass NemoActionClass; - -typedef enum { - SELECTION_SINGLE = G_MAXINT - 10, - SELECTION_MULTIPLE, - SELECTION_NOT_NONE, - SELECTION_ANY, - SELECTION_NONE -} SelectionType; - -typedef enum { - QUOTE_TYPE_SINGLE = 0, - QUOTE_TYPE_DOUBLE, - QUOTE_TYPE_BACKTICK, - QUOTE_TYPE_NONE -} QuoteType; - -typedef enum { - TOKEN_NONE = 0, - TOKEN_PATH_LIST, - TOKEN_URI_LIST, - TOKEN_FILE_DISPLAY_NAME, - TOKEN_PARENT_DISPLAY_NAME, - TOKEN_PARENT_PATH, - TOKEN_PARENT_URI, - TOKEN_DEVICE, - TOKEN_FILE_DISPLAY_NAME_NO_EXT, - TOKEN_LITERAL_PERCENT, - TOKEN_XID -} TokenType; +#define NEMO_TYPE_ACTION nemo_action_get_type() +G_DECLARE_FINAL_TYPE (NemoAction, nemo_action, NEMO, ACTION, GtkAction) struct _NemoAction { - GtkAction parent; + GtkAction parent_instance; + + gchar *uuid; // basename of key_file_path gchar *key_file_path; - SelectionType selection_type; - gchar **extensions; - gchar **mimetypes; - gchar *exec; gchar *parent_dir; - gchar **conditions; - gchar *separator; - QuoteType quote_type; - gchar *orig_label; - gchar *orig_tt; - gboolean use_parent_dir; - GList *dbus; - guint dbus_recalc_timeout_id; - GList *gsettings; - guint gsettings_recalc_timeout_id; - gboolean dbus_satisfied; - gboolean gsettings_satisfied; - gboolean escape_underscores; - gboolean escape_space; - gboolean show_in_blank_desktop; - gboolean run_in_terminal; - gchar *uri_scheme; - - gboolean constructing; }; struct _NemoActionClass { - GtkActionClass parent_class; + GtkActionClass parent_class; }; -GType nemo_action_get_type (void); NemoAction *nemo_action_new (const gchar *name, const gchar *path); void nemo_action_activate (NemoAction *action, GList *selection, NemoFile *parent, GtkWindow *window); @@ -150,6 +49,9 @@ const gchar *nemo_action_get_orig_label (NemoAction *action); const gchar *nemo_action_get_orig_tt (NemoAction *action); gchar *nemo_action_get_label (NemoAction *action, GList *selection, NemoFile *parent, GtkWindow *window); gchar *nemo_action_get_tt (NemoAction *action, GList *selection, NemoFile *parent, GtkWindow *window); -gboolean nemo_action_get_visibility (NemoAction *action, GList *selection, NemoFile *parent, gboolean for_places, GtkWindow *window); +void nemo_action_update_display_state (NemoAction *action, GList *selection, NemoFile *parent, gboolean for_places, GtkWindow *window); +// Layout model overrides +void nemo_action_override_label (NemoAction *action, const gchar *label); +void nemo_action_override_icon (NemoAction *action, const gchar *icon_name); #endif /* NEMO_ACTION_H */ diff --git a/libnemo-private/nemo-ui-utilities.c b/libnemo-private/nemo-ui-utilities.c index 8d873178e..45fe94a12 100644 --- a/libnemo-private/nemo-ui-utilities.c +++ b/libnemo-private/nemo-ui-utilities.c @@ -134,3 +134,16 @@ nemo_ui_get_menu_icon (const char *icon_name, return pixbuf; } + +gchar * +nemo_make_action_uuid_for_path (const gchar *path) +{ + g_autofree gchar *copy = g_path_get_basename (path); + g_strdelimit (copy, " ", '_'); + + if (!g_str_has_suffix (copy, ".nemo_action")) { + return copy; + } + + return g_strndup (copy, strlen (copy) - strlen (".nemo_action")); +} \ No newline at end of file diff --git a/libnemo-private/nemo-ui-utilities.h b/libnemo-private/nemo-ui-utilities.h index f1e7d73cb..44d1a8adb 100644 --- a/libnemo-private/nemo-ui-utilities.h +++ b/libnemo-private/nemo-ui-utilities.h @@ -39,5 +39,5 @@ GtkAction * nemo_action_from_menu_item (NemoMenuItem *item, GdkPixbuf * nemo_ui_get_menu_icon (const char *icon_name, GtkWidget *parent_widget); - +gchar * nemo_make_action_uuid_for_path (const gchar *path); #endif /* NEMO_UI_UTILITIES_H */ diff --git a/meson.build b/meson.build index bba97ee31..daa6c9cbe 100644 --- a/meson.build +++ b/meson.build @@ -79,6 +79,7 @@ glib = dependency('glib-2.0', version: glib_version) gmodule = dependency('gmodule-no-export-2.0', version: glib_version) gobject = dependency('gobject-2.0', version: '>=2.0') go_intr = dependency('gobject-introspection-1.0', version: '>=1.0') +json = dependency('json-glib-1.0', version: '>=1.6') cinnamon= dependency('cinnamon-desktop', version: '>=4.8.0') gail = dependency('gail-3.0') @@ -191,6 +192,7 @@ subdir('src') subdir('search-helpers') subdir('test') subdir('docs') +subdir('action-layout-editor') message('\n'.join(['', ' @0@-@1@'.format(meson.project_name(), meson.project_version()), diff --git a/src/nemo-action-config-widget.c b/src/nemo-action-config-widget.c index 4cf6b1526..442432d7b 100644 --- a/src/nemo-action-config-widget.c +++ b/src/nemo-action-config-widget.c @@ -11,7 +11,7 @@ #include "nemo-file.h" #include #include -#include +#include #include "nemo-global-preferences.h" G_DEFINE_TYPE (NemoActionConfigWidget, nemo_action_config_widget, NEMO_TYPE_CONFIG_BASE_WIDGET); @@ -357,6 +357,12 @@ on_open_folder_clicked (GtkWidget *button, NemoActionConfigWidget *widget) g_object_unref (location); } +static void +on_layout_editor_clicked (GtkWidget *button, NemoActionConfigWidget *widget) +{ + g_spawn_command_line_async ("nemo-action-layout-editor", NULL); +} + static void on_dir_changed (GFileMonitor *monitor, GFile *file, @@ -461,9 +467,15 @@ nemo_action_config_widget_init (NemoActionConfigWidget *self) widget, FALSE, FALSE, 0); gtk_widget_show (widget); - g_signal_connect (widget, "clicked", G_CALLBACK (on_open_folder_clicked), self); + widget = gtk_button_new_with_label (_("Layout")); + gtk_box_pack_start (GTK_BOX (bb), + widget, + FALSE, FALSE, 0); + gtk_widget_show (widget); + g_signal_connect (widget, "clicked", G_CALLBACK (on_layout_editor_clicked), self); + g_signal_connect (nemo_config_base_widget_get_enable_button (NEMO_CONFIG_BASE_WIDGET (self)), "clicked", G_CALLBACK (on_enable_clicked), self); diff --git a/src/nemo-actions.h b/src/nemo-actions.h index 3126e9058..0ec1c0722 100644 --- a/src/nemo-actions.h +++ b/src/nemo-actions.h @@ -161,6 +161,9 @@ #define NEMO_ACTION_UNFAVORITE_FILE "Unfavorite File" #define NEMO_ACTION_DESKTOP_OVERLAY "Desktop Overlay" +#define NEMO_ACTION_SIDEBAR_REMOVE "Remove Bookmark" +#define NEMO_ACTION_SIDEBAR_DETECT_MEDIA "Detect Media" + typedef struct { const gchar *action_name; // The action's name diff --git a/src/nemo-blank-desktop-window.c b/src/nemo-blank-desktop-window.c index 7943940d7..70c5ba710 100644 --- a/src/nemo-blank-desktop-window.c +++ b/src/nemo-blank-desktop-window.c @@ -34,6 +34,7 @@ #include #include #include +#include #include @@ -51,147 +52,208 @@ static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; struct NemoBlankDesktopWindowDetails { gint monitor; + GtkWidget *event_box; GtkWidget *popup_menu; gulong actions_changed_id; + guint actions_changed_idle_id; + + gboolean actions_need_update; + + NemoActionManager *action_manager; + GtkUIManager *ui_manager; + GtkActionGroup *action_group; + guint actions_merge_id; }; G_DEFINE_TYPE (NemoBlankDesktopWindow, nemo_blank_desktop_window, GTK_TYPE_WINDOW); +static void update_actions_visibility (NemoBlankDesktopWindow *window); +static void update_actions_menu (NemoBlankDesktopWindow *window); +static void actions_changed (gpointer user_data); + static void -customize_clicked_callback (GtkMenuItem *item, - NemoBlankDesktopWindow *window) +action_show_overlay (GtkAction *action, gpointer user_data) { - g_return_if_fail (NEMO_IS_BLANK_DESKTOP_WINDOW (window)); + g_return_if_fail (NEMO_IS_BLANK_DESKTOP_WINDOW (user_data)); + NemoBlankDesktopWindow *window = NEMO_BLANK_DESKTOP_WINDOW (user_data); nemo_desktop_manager_show_desktop_overlay (nemo_desktop_manager_get (), window->details->monitor); } -static void -action_activated_callback (GtkMenuItem *item, NemoAction *action) -{ - GFile *desktop_location = nemo_get_desktop_location (); - NemoFile *desktop_file = nemo_file_get (desktop_location); - GtkWindow *window = g_object_get_data (G_OBJECT (item), "nemo-window"); - g_object_unref (desktop_location); - - nemo_action_activate (NEMO_ACTION (action), NULL, desktop_file, GTK_WINDOW (window)); -} +static const GtkActionEntry entries[] = { + { "Desktop Overlay", NULL, N_("_Customize"), NULL, N_("Adjust the desktop layout for this monitor"), G_CALLBACK (action_show_overlay) } + // + // +}; static void reset_popup_menu (NemoBlankDesktopWindow *window) { - g_clear_pointer (&window->details->popup_menu, gtk_widget_destroy); + window->details->actions_need_update = TRUE; + update_actions_menu (window); } static void -build_menu (NemoBlankDesktopWindow *window) +do_popup_menu (NemoBlankDesktopWindow *window, GdkEventButton *event) { - if (window->details->popup_menu) { + if (window->details->popup_menu == NULL) { return; } - gboolean show_customize; - NemoActionManager *desktop_action_manager = nemo_desktop_manager_get_action_manager (); + update_actions_visibility (window); - if (window->details->actions_changed_id == 0) { - window->details->actions_changed_id = g_signal_connect_swapped (desktop_action_manager, - "changed", - G_CALLBACK (reset_popup_menu), - window); - } + eel_pop_up_context_menu (GTK_MENU(window->details->popup_menu), + (GdkEvent *) event, + GTK_WIDGET (window)); +} - show_customize = g_settings_get_boolean (nemo_menu_config_preferences, "desktop-menu-customize"); - GList *action_list = nemo_action_manager_list_actions (desktop_action_manager); +static gboolean +on_popup_menu (GtkWidget *widget, NemoBlankDesktopWindow *window) +{ + do_popup_menu (window, NULL); + return TRUE; +} - if (g_list_length (action_list) == 0 && !show_customize) - return; +static gboolean +on_button_press (GtkWidget *widget, GdkEventButton *event, NemoBlankDesktopWindow *window) +{ + if (event->type != GDK_BUTTON_PRESS) { + /* ignore multiple clicks */ + return TRUE; + } + + if (event->button == 3) { + do_popup_menu (window, event); + } - window->details->popup_menu = gtk_menu_new (); + return FALSE; +} - gboolean show; - g_object_get (gtk_settings_get_default (), "gtk-menu-images", &show, NULL); +static void +run_action_callback (NemoAction *action, gpointer user_data) +{ + nemo_action_activate (action, NULL, NULL, GTK_WINDOW (user_data)); +} - gtk_menu_attach_to_widget (GTK_MENU (window->details->popup_menu), - GTK_WIDGET (window), - NULL); +static void +update_actions_visibility (NemoBlankDesktopWindow *window) +{ + nemo_action_manager_update_action_states (window->details->action_manager, + window->details->action_group, + NULL, + NULL, + FALSE, + GTK_WINDOW (window)); +} - GtkWidget *item; - GList *l; - NemoAction *action; +static void +add_action_to_ui (NemoActionManager *manager, + GtkAction *action, + GtkUIManagerItemType type, + const gchar *path, + gpointer user_data) +{ + NemoBlankDesktopWindow *window = NEMO_BLANK_DESKTOP_WINDOW (user_data); - for (l = action_list; l != NULL; l = l->next) { - action = l->data; + if (type != GTK_UI_MANAGER_SEPARATOR) { + if (type == GTK_UI_MANAGER_MENUITEM) { + g_signal_handlers_disconnect_by_func (action, + run_action_callback, + window); - if (action->show_in_blank_desktop && action->dbus_satisfied && action->gsettings_satisfied) { - gchar *label = nemo_action_get_label (action, NULL, NULL, GTK_WINDOW (window)); - item = gtk_image_menu_item_new_with_mnemonic (label); - g_free (label); + g_signal_connect (action, "activate", + G_CALLBACK (run_action_callback), + window); + } - const gchar *stock_id = gtk_action_get_stock_id (GTK_ACTION (action)); - const gchar *icon_name = gtk_action_get_icon_name (GTK_ACTION (action)); + gtk_action_group_add_action (window->details->action_group, + action); + gtk_action_set_visible (GTK_ACTION (action), FALSE); + } - if (stock_id || icon_name) { - GtkWidget *image = stock_id ? gtk_image_new_from_stock (stock_id, GTK_ICON_SIZE_MENU) : - gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU); + const gchar *placeholder = "/background/BlankDesktopActionsPlaceholder"; - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image); - gtk_image_menu_item_set_always_show_image (GTK_IMAGE_MENU_ITEM (item), show); - } + g_autofree gchar *full_path = NULL; + const gchar *name; - gtk_widget_set_visible (item, TRUE); - g_object_set_data (G_OBJECT (item), "nemo-window", window); - g_signal_connect (item, "activate", G_CALLBACK (action_activated_callback), action); - gtk_menu_shell_append (GTK_MENU_SHELL (window->details->popup_menu), item); - } + if (path != NULL) { + full_path = g_strdup_printf ("%s/%s", placeholder, path); + } + else { + full_path = g_strdup (placeholder); } - if (!show_customize) { - return; + + if (type == GTK_UI_MANAGER_SEPARATOR) { + name = NULL; + } + else { + name = gtk_action_get_name (action); } - item = gtk_menu_item_new_with_label (_("Customize")); + gtk_ui_manager_add_ui (window->details->ui_manager, + window->details->actions_merge_id, + full_path, + name, + name, + type, + FALSE); +} - gtk_widget_set_visible (item, TRUE); - g_signal_connect (item, "activate", G_CALLBACK (customize_clicked_callback), window); - gtk_menu_shell_append (GTK_MENU_SHELL (window->details->popup_menu), item); +static void +clear_ui (NemoBlankDesktopWindow *window) +{ + if (window->details->actions_merge_id > 0) { + gtk_ui_manager_remove_ui (window->details->ui_manager, + window->details->actions_merge_id); + window->details->actions_merge_id = 0;; + + gtk_ui_manager_remove_action_group (window->details->ui_manager, + window->details->action_group); + g_object_unref (window->details->action_group); + } } static void -do_popup_menu (NemoBlankDesktopWindow *window, GdkEventButton *event) +update_actions_menu (NemoBlankDesktopWindow *window) { - build_menu (window); + if (!gtk_widget_get_realized (GTK_WIDGET (window))) { + return; + } - if (window->details->popup_menu == NULL) { + if (!window->details->actions_need_update) { return; } - eel_pop_up_context_menu (GTK_MENU(window->details->popup_menu), - (GdkEvent *) event, - GTK_WIDGET (window)); -} + clear_ui (window); -static gboolean -on_popup_menu (GtkWidget *widget, NemoBlankDesktopWindow *window) -{ - do_popup_menu (window, NULL); - return TRUE; -} + window->details->action_group = gtk_action_group_new ("NemoBlankDesktopActions"); + gtk_action_group_set_translation_domain (window->details->action_group, GETTEXT_PACKAGE); + gtk_ui_manager_insert_action_group (window->details->ui_manager, window->details->action_group, 0); -static gboolean -on_button_press (GtkWidget *widget, GdkEventButton *event, NemoBlankDesktopWindow *window) -{ - if (event->type != GDK_BUTTON_PRESS) { - /* ignore multiple clicks */ - return TRUE; - } + window->details->actions_merge_id = + gtk_ui_manager_add_ui_from_resource (window->details->ui_manager, "/org/nemo/nemo-blank-desktop-window-ui.xml", NULL); - if (event->button == 3) { - do_popup_menu (window, event); + nemo_action_manager_iterate_actions (window->details->action_manager, + (NemoActionManagerIterFunc) add_action_to_ui, + window); + + gtk_action_group_add_actions (window->details->action_group, + entries, + G_N_ELEMENTS (entries), + window); + + if (window->details->popup_menu == NULL) { + GtkWidget *menu = gtk_ui_manager_get_widget (window->details->ui_manager, "/background"); + gtk_menu_set_screen (GTK_MENU (menu), gtk_widget_get_screen (GTK_WIDGET (window))); + window->details->popup_menu = menu; + + gtk_widget_show (menu); } - return FALSE; + window->details->actions_need_update = FALSE; } static void @@ -205,6 +267,8 @@ nemo_blank_desktop_window_dispose (GObject *obj) window->details->actions_changed_id = 0; } + clear_ui (window); + g_signal_handlers_disconnect_by_func (nemo_menu_config_preferences, reset_popup_menu, window); G_OBJECT_CLASS (nemo_blank_desktop_window_parent_class)->dispose (obj); @@ -213,9 +277,33 @@ nemo_blank_desktop_window_dispose (GObject *obj) static void nemo_blank_desktop_window_finalize (GObject *obj) { + NemoBlankDesktopWindow *window = NEMO_BLANK_DESKTOP_WINDOW (obj); + + g_object_unref (window->details->ui_manager); + G_OBJECT_CLASS (nemo_blank_desktop_window_parent_class)->finalize (obj); } +static gboolean +actions_changed_idle_cb (gpointer user_data) +{ + NemoBlankDesktopWindow *window = NEMO_BLANK_DESKTOP_WINDOW (user_data); + + reset_popup_menu (window); + + window->details->actions_changed_idle_id = 0; + return G_SOURCE_REMOVE; +} + +static void +actions_changed (gpointer user_data) +{ + NemoBlankDesktopWindow *window = NEMO_BLANK_DESKTOP_WINDOW (user_data); + + g_clear_handle_id (&window->details->actions_changed_idle_id, g_source_remove); + window->details->actions_changed_idle_id = g_idle_add (actions_changed_idle_cb, window); +} + static void nemo_blank_desktop_window_constructed (GObject *obj) { @@ -231,11 +319,15 @@ nemo_blank_desktop_window_constructed (GObject *obj) atk_object_set_name (accessible, _("Desktop")); } - /* We don't want this extra action manager unless there's actually - * a blank desktop in use. If so, however, we need to initialize it early here, - * as it populates itself with actions asynchronously, and would not be ready - * in build_menu().*/ - nemo_desktop_manager_get_action_manager (); + window->details->ui_manager = gtk_ui_manager_new (); + window->details->action_manager = nemo_action_manager_new (); + + if (window->details->actions_changed_id == 0) { + window->details->actions_changed_id = g_signal_connect_swapped (window->details->action_manager, + "changed", + G_CALLBACK (actions_changed), + window); + } nemo_blank_desktop_window_update_geometry (window); @@ -244,10 +336,14 @@ nemo_blank_desktop_window_constructed (GObject *obj) gtk_window_set_decorated (GTK_WINDOW (window), FALSE); + window->details->event_box = gtk_event_box_new (); + gtk_event_box_set_visible_window (GTK_EVENT_BOX (window->details->event_box), FALSE); + gtk_container_add (GTK_CONTAINER (window), window->details->event_box); + gtk_widget_show_all (GTK_WIDGET (window)); - g_signal_connect (GTK_WIDGET (window), "button-press-event", G_CALLBACK (on_button_press), window); - g_signal_connect (GTK_WIDGET (window), "popup-menu", G_CALLBACK (on_popup_menu), window); + g_signal_connect (window->details->event_box, "button-press-event", G_CALLBACK (on_button_press), window); + g_signal_connect (window->details->event_box, "popup-menu", G_CALLBACK (on_popup_menu), window); g_signal_connect_swapped (nemo_menu_config_preferences, "changed::desktop-menu-customize", @@ -299,6 +395,7 @@ nemo_blank_desktop_window_init (NemoBlankDesktopWindow *window) window->details->popup_menu = NULL; window->details->actions_changed_id = 0; + gtk_window_set_type_hint (GTK_WINDOW (window), GDK_WINDOW_TYPE_HINT_DESKTOP); /* Make it easier for themes authors to style the desktop window separately */ gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (window)), "nemo-desktop-window"); @@ -347,19 +444,6 @@ unrealize (GtkWidget *widget) GTK_WIDGET_CLASS (nemo_blank_desktop_window_parent_class)->unrealize (widget); } -static void -set_wmspec_desktop_hint (GdkWindow *window) -{ - GdkAtom atom; - - atom = gdk_atom_intern ("_NET_WM_WINDOW_TYPE_DESKTOP", FALSE); - - gdk_property_change (window, - gdk_atom_intern ("_NET_WM_WINDOW_TYPE", FALSE), - gdk_x11_xatom_to_atom (XA_ATOM), 32, - GDK_PROP_MODE_REPLACE, (guchar *) &atom, 1); -} - static void realize (GtkWidget *widget) { @@ -371,9 +455,6 @@ realize (GtkWidget *widget) } GTK_WIDGET_CLASS (nemo_blank_desktop_window_parent_class)->realize (widget); - - /* This is the new way to set up the desktop window */ - set_wmspec_desktop_hint (gtk_widget_get_window (widget)); } static void diff --git a/src/nemo-places-sidebar.c b/src/nemo-places-sidebar.c index a81655835..f7db112b7 100644 --- a/src/nemo-places-sidebar.c +++ b/src/nemo-places-sidebar.c @@ -45,6 +45,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,7 @@ #include #include +#include "nemo-actions.h" #include "nemo-application.h" #include "nemo-bookmark-list.h" #include "nemo-places-sidebar.h" @@ -87,8 +89,17 @@ typedef struct { GVolumeMonitor *volume_monitor; NemoActionManager *action_manager; - guint action_manager_changed_id; - GList *action_items; + gulong actions_changed_id; + guint actions_changed_idle_id; + + GtkUIManager *ui_manager; + + GtkActionGroup *bookmark_action_group; + guint bookmark_action_group_merge_id; + GtkActionGroup *action_action_group; + guint action_action_group_merge_id; + + gboolean actions_need_update; gboolean devices_header_added; gboolean bookmarks_header_added; @@ -103,22 +114,6 @@ typedef struct { gboolean desktop_dnd_can_delete_source; GtkWidget *popup_menu; - GtkWidget *popup_menu_open_in_new_tab_item; - GtkWidget *popup_menu_add_shortcut_item; - GtkWidget *popup_menu_remove_item; - GtkWidget *popup_menu_rename_item; - GtkWidget *popup_menu_separator_item; - GtkWidget *popup_menu_mount_item; - GtkWidget *popup_menu_unmount_item; - GtkWidget *popup_menu_eject_item; - GtkWidget *popup_menu_rescan_item; - GtkWidget *popup_menu_empty_trash_item; - GtkWidget *popup_menu_start_item; - GtkWidget *popup_menu_stop_item; - GtkWidget *popup_menu_properties_separator_item; - GtkWidget *popup_menu_properties_item; - GtkWidget *popup_menu_action_separator_item; - GtkWidget *popup_menu_remove_rename_separator_item; /* volume mounting - delayed open process */ gboolean mounting; @@ -143,11 +138,8 @@ typedef struct { gchar *bottom_bookend_uri; gint bookmark_breakpoint; - guint expand_timeout_source; - guint popup_menu_action_index; - guint update_places_on_idle_id; } NemoPlacesSidebar; @@ -229,14 +221,10 @@ static void check_unmount_and_eject (GMount *mount, gboolean *show_unmount, gboolean *show_eject); -static void bookmarks_check_popup_sensitivity (NemoPlacesSidebar *sidebar); - -static void add_action_popup_items (NemoPlacesSidebar *sidebar); - static void update_places (NemoPlacesSidebar *sidebar); - static void update_places_on_idle (NemoPlacesSidebar *sidebar); - +static void rebuild_menu (NemoPlacesSidebar *sidebar); +static void actions_changed (gpointer user_data); /* Identifiers for target types */ enum { GTK_TREE_MODEL_ROW, @@ -679,7 +667,7 @@ get_disk_full (GFile *file, gchar **tooltip_info) } if (error != NULL) { - g_printerr ("Couldn't get disk full info for: %s\n", error->message); + g_warning ("Couldn't get disk full info for: %s", error->message); g_clear_error (&error); } @@ -1305,6 +1293,8 @@ update_places (NemoPlacesSidebar *sidebar) restore_expand_state (sidebar); sidebar_update_restore_selection (sidebar, location, last_uri); + actions_changed (sidebar); + sidebar->updating_sidebar = FALSE; g_free (location); @@ -2215,34 +2205,6 @@ drag_drop_callback (GtkTreeView *tree_view, return retval; } -/* Callback used when the file list's popup menu is detached */ -static void -bookmarks_popup_menu_detach_cb (GtkWidget *attach_widget, - GtkMenu *menu) -{ - NemoPlacesSidebar *sidebar; - - sidebar = NEMO_PLACES_SIDEBAR (attach_widget); - g_assert (NEMO_IS_PLACES_SIDEBAR (sidebar)); - - sidebar->popup_menu = NULL; - sidebar->popup_menu_add_shortcut_item = NULL; - sidebar->popup_menu_remove_item = NULL; - sidebar->popup_menu_rename_item = NULL; - sidebar->popup_menu_separator_item = NULL; - sidebar->popup_menu_mount_item = NULL; - sidebar->popup_menu_unmount_item = NULL; - sidebar->popup_menu_eject_item = NULL; - sidebar->popup_menu_rescan_item = NULL; - sidebar->popup_menu_start_item = NULL; - sidebar->popup_menu_stop_item = NULL; - sidebar->popup_menu_empty_trash_item = NULL; - sidebar->popup_menu_properties_separator_item = NULL; - sidebar->popup_menu_properties_item = NULL; - sidebar->popup_menu_action_separator_item = NULL; - sidebar->popup_menu_remove_rename_separator_item = NULL; -} - static void check_unmount_and_eject (GMount *mount, GVolume *volume, @@ -2303,19 +2265,19 @@ check_visibility (GMount *mount, } } -static void hide_all_action_items (NemoPlacesSidebar *sidebar) +static void +set_action_visible (GtkActionGroup *action_group, + const gchar *name, + gboolean visible) { - GList *l; - ActionPayload *p; + GtkAction *action; - for (l = sidebar->action_items; l != NULL; l = l->next) { - p = l->data; - gtk_widget_set_visible (p->item, FALSE); - } + action = gtk_action_group_get_action (action_group, name); + gtk_action_set_visible (action, visible); } static void -bookmarks_check_popup_sensitivity (NemoPlacesSidebar *sidebar) +update_menu_states (NemoPlacesSidebar *sidebar) { GtkTreeIter iter; PlaceType type; @@ -2350,14 +2312,26 @@ bookmarks_check_popup_sensitivity (NemoPlacesSidebar *sidebar) -1); } - gtk_widget_set_visible (sidebar->popup_menu_remove_rename_separator_item, (type == PLACES_MOUNTED_VOLUME || - type == PLACES_BOOKMARK)); + if (uri) { + NemoFile *file = nemo_file_get_by_uri (uri); + NemoFile *parent = nemo_file_get_parent (file); - gtk_widget_set_visible (sidebar->popup_menu_add_shortcut_item, (type == PLACES_MOUNTED_VOLUME)); + GList *selection = g_list_prepend (NULL, file); - gtk_widget_set_visible (sidebar->popup_menu_remove_item, (type == PLACES_BOOKMARK)); - gtk_widget_set_visible (sidebar->popup_menu_rename_item, (type == PLACES_BOOKMARK)); - gtk_widget_set_sensitive (sidebar->popup_menu_empty_trash_item, !nemo_trash_monitor_is_empty ()); + nemo_action_manager_update_action_states (sidebar->action_manager, + sidebar->action_action_group, + selection, + parent, + TRUE, + GTK_WINDOW (sidebar->window)); + nemo_file_list_free (selection); + nemo_file_unref (parent); + } + + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_ADD_BOOKMARK, (type == PLACES_MOUNTED_VOLUME)); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_SIDEBAR_REMOVE, (type == PLACES_BOOKMARK)); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_RENAME, (type == PLACES_BOOKMARK)); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_EMPTY_TRASH_CONDITIONAL, !nemo_trash_monitor_is_empty ()); check_visibility (mount, volume, drive, &show_mount, &show_unmount, &show_eject, &show_rescan, &show_start, &show_stop); @@ -2365,10 +2339,11 @@ bookmarks_check_popup_sensitivity (NemoPlacesSidebar *sidebar) /* We actually want both eject and unmount since eject will unmount all volumes. * TODO: hide unmount if the drive only has a single mountable volume */ - show_empty_trash = (uri != NULL) && (!strcmp (uri, "trash:///")); + g_free (uri); + /* Only show properties for local mounts */ show_properties = (mount != NULL); if (mount != NULL) { @@ -2381,40 +2356,42 @@ bookmarks_check_popup_sensitivity (NemoPlacesSidebar *sidebar) g_object_unref (location); } - gtk_widget_set_visible (sidebar->popup_menu_separator_item, - show_mount || show_unmount || show_eject || show_empty_trash); - gtk_widget_set_visible (sidebar->popup_menu_mount_item, show_mount); - gtk_widget_set_visible (sidebar->popup_menu_unmount_item, show_unmount); - gtk_widget_set_visible (sidebar->popup_menu_eject_item, show_eject); - gtk_widget_set_visible (sidebar->popup_menu_rescan_item, show_rescan); - gtk_widget_set_visible (sidebar->popup_menu_start_item, show_start); - gtk_widget_set_visible (sidebar->popup_menu_stop_item, show_stop); - gtk_widget_set_visible (sidebar->popup_menu_empty_trash_item, show_empty_trash); - gtk_widget_set_visible (sidebar->popup_menu_properties_separator_item, show_properties); - gtk_widget_set_visible (sidebar->popup_menu_properties_item, show_properties); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_MOUNT_VOLUME, show_mount); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_UNMOUNT_VOLUME, show_unmount); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_EJECT_VOLUME, show_eject); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_SIDEBAR_DETECT_MEDIA, show_rescan); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_START_VOLUME, show_start); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_STOP_VOLUME, show_stop); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_EMPTY_TRASH_CONDITIONAL, show_empty_trash); + set_action_visible (sidebar->bookmark_action_group, NEMO_ACTION_PROPERTIES, show_properties); /* Adjust start/stop items to reflect the type of the drive */ - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_start_item), _("_Start")); - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_stop_item), _("_Stop")); + GtkAction *start_action, *stop_action; + + start_action = gtk_action_group_get_action (sidebar->bookmark_action_group, NEMO_ACTION_START_VOLUME); + stop_action = gtk_action_group_get_action (sidebar->bookmark_action_group, NEMO_ACTION_STOP_VOLUME); + + gtk_action_set_label (start_action, _("_Start")); + gtk_action_set_label (stop_action, _("_Stop")); if ((show_start || show_stop) && drive != NULL) { switch (g_drive_get_start_stop_type (drive)) { case G_DRIVE_START_STOP_TYPE_SHUTDOWN: /* start() for type G_DRIVE_START_STOP_TYPE_SHUTDOWN is normally not used */ - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_start_item), _("_Power On")); - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_stop_item), _("_Safely Remove Drive")); + gtk_action_set_label (start_action, _("_Power On")); + gtk_action_set_label (stop_action, _("_Safely Remove Drive")); break; case G_DRIVE_START_STOP_TYPE_NETWORK: - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_start_item), _("_Connect Drive")); - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_stop_item), _("_Disconnect Drive")); + gtk_action_set_label (start_action, _("_Connect Drive")); + gtk_action_set_label (stop_action, _("_Disconnect Drive")); break; case G_DRIVE_START_STOP_TYPE_MULTIDISK: - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_start_item), _("_Start Multi-disk Device")); - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_stop_item), _("_Stop Multi-disk Device")); + gtk_action_set_label (start_action, _("_Start Multi-disk Device")); + gtk_action_set_label (stop_action, _("_Stop Multi-disk Device")); break; case G_DRIVE_START_STOP_TYPE_PASSWORD: /* stop() for type G_DRIVE_START_STOP_TYPE_PASSWORD is normally not used */ - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_start_item), _("_Unlock Drive")); - gtk_menu_item_set_label (GTK_MENU_ITEM (sidebar->popup_menu_stop_item), _("_Lock Drive")); + gtk_action_set_label (start_action, _("_Unlock Drive")); + gtk_action_set_label (stop_action, _("_Lock Drive")); break; default: @@ -2427,43 +2404,6 @@ bookmarks_check_popup_sensitivity (NemoPlacesSidebar *sidebar) g_clear_object (&drive); g_clear_object (&volume); g_clear_object (&mount); - - if (!uri) { - hide_all_action_items (sidebar); - gtk_widget_set_visible (sidebar->popup_menu_action_separator_item, FALSE); - return; - } - - gboolean actions_visible = FALSE; - - GList *l; - NemoFile *file = nemo_file_get_by_uri (uri); - - NemoFile *parent = nemo_file_get_parent (file); - GList *tmp = NULL; - tmp = g_list_append (tmp, file); - ActionPayload *p; - - for (l = sidebar->action_items; l != NULL; l = l->next) { - p = l->data; - if (nemo_action_get_visibility (p->action, tmp, parent, TRUE, GTK_WINDOW (sidebar->window))) { - gchar *action_label = nemo_action_get_label (p->action, tmp, parent, GTK_WINDOW (sidebar->window)); - - gtk_menu_item_set_label (GTK_MENU_ITEM (p->item), action_label); - gtk_widget_set_visible (p->item, TRUE); - - g_free (action_label); - actions_visible = TRUE; - } else { - gtk_widget_set_visible (p->item, FALSE); - } - } - - gtk_widget_set_visible (sidebar->popup_menu_action_separator_item, actions_visible); - - nemo_file_list_free (tmp); - nemo_file_unref (parent); - g_free (uri); } /* Callback used when the selection in the shortcuts tree changes */ @@ -2471,7 +2411,7 @@ static void bookmarks_selection_changed_cb (GtkTreeSelection *selection, NemoPlacesSidebar *sidebar) { - bookmarks_check_popup_sensitivity (sidebar); + update_menu_states (sidebar); } static void @@ -2635,21 +2575,21 @@ open_shortcut_from_menu (NemoPlacesSidebar *sidebar, } static void -open_shortcut_cb (GtkMenuItem *item, +open_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { open_shortcut_from_menu (sidebar, 0); } static void -open_shortcut_in_new_window_cb (GtkMenuItem *item, +open_shortcut_in_new_window_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { open_shortcut_from_menu (sidebar, NEMO_WINDOW_OPEN_FLAG_NEW_WINDOW); } static void -open_shortcut_in_new_tab_cb (GtkMenuItem *item, +open_shortcut_in_new_tab_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { open_shortcut_from_menu (sidebar, NEMO_WINDOW_OPEN_FLAG_NEW_TAB); @@ -2688,7 +2628,7 @@ add_bookmark (NemoPlacesSidebar *sidebar) } static void -add_shortcut_cb (GtkMenuItem *item, +add_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { add_bookmark (sidebar); @@ -2727,7 +2667,7 @@ rename_selected_bookmark (NemoPlacesSidebar *sidebar) } static void -rename_shortcut_cb (GtkMenuItem *item, +rename_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { rename_selected_bookmark (sidebar); @@ -2764,14 +2704,14 @@ remove_selected_bookmarks (NemoPlacesSidebar *sidebar) } static void -remove_shortcut_cb (GtkMenuItem *item, +remove_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { remove_selected_bookmarks (sidebar); } static void -mount_shortcut_cb (GtkMenuItem *item, +mount_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { GtkTreeIter iter; @@ -2874,7 +2814,7 @@ do_unmount_selection (NemoPlacesSidebar *sidebar) } static void -unmount_shortcut_cb (GtkMenuItem *item, +unmount_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { do_unmount_selection (sidebar); @@ -2999,7 +2939,7 @@ do_eject (GMount *mount, } static void -eject_shortcut_cb (GtkMenuItem *item, +eject_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { GtkTreeIter iter; @@ -3116,7 +3056,7 @@ drive_poll_for_media_cb (GObject *source_object, } static void -rescan_shortcut_cb (GtkMenuItem *item, +rescan_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { GtkTreeIter iter; @@ -3160,7 +3100,7 @@ drive_start_cb (GObject *source_object, } static void -start_shortcut_cb (GtkMenuItem *item, +start_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { GtkTreeIter iter; @@ -3214,7 +3154,7 @@ drive_stop_cb (GObject *source_object, } static void -stop_shortcut_cb (GtkMenuItem *item, +stop_shortcut_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { GtkTreeIter iter; @@ -3238,7 +3178,7 @@ stop_shortcut_cb (GtkMenuItem *item, } static void -empty_trash_cb (GtkMenuItem *item, +empty_trash_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { nemo_file_operations_empty_trash (GTK_WIDGET (sidebar->window)); @@ -3288,7 +3228,7 @@ find_next_row (NemoPlacesSidebar *sidebar, GtkTreeIter *iter) } static void -properties_cb (GtkMenuItem *item, +properties_cb (GtkAction *item, NemoPlacesSidebar *sidebar) { GtkTreeModel *model; @@ -3427,13 +3367,12 @@ bookmarks_key_press_event_cb (GtkWidget *widget, } static void -action_activated_callback (GtkMenuItem *item, ActionPayload *payload) +run_action_callback (GtkAction *action, gpointer user_data) { + NemoPlacesSidebar *sidebar = NEMO_PLACES_SIDEBAR (user_data); gchar *uri = NULL; GtkTreeIter iter; - NemoPlacesSidebar *sidebar = payload->sidebar; - if (get_selected_iter (sidebar, &iter)) { gtk_tree_model_get (GTK_TREE_MODEL (sidebar->store_filter), &iter, PLACES_SIDEBAR_COLUMN_URI, &uri, @@ -3446,48 +3385,16 @@ action_activated_callback (GtkMenuItem *item, ActionPayload *payload) NemoFile *file = nemo_file_get_by_uri (uri); NemoFile *parent = nemo_file_get_parent (file); - GList *tmp = NULL; - tmp = g_list_append (tmp, file); + GList *selection = g_list_prepend (NULL, file); - nemo_action_activate (NEMO_ACTION (payload->action), tmp, parent, GTK_WINDOW (sidebar->window)); + nemo_action_activate (NEMO_ACTION (action), selection, parent, GTK_WINDOW (sidebar->window)); - nemo_file_list_free (tmp); + nemo_file_list_free (selection); nemo_file_unref (parent); g_free (uri); } -static void -add_action_popup_items (NemoPlacesSidebar *sidebar) -{ - if (sidebar->action_items != NULL) - g_list_free_full (sidebar->action_items, g_free); - - sidebar->action_items = NULL; - - GList *action_list = nemo_action_manager_list_actions (sidebar->action_manager); - GtkWidget *item; - GList *l; - NemoAction *action; - ActionPayload *payload; - - guint index = 8; - - for (l = action_list; l != NULL; l = l->next) { - action = l->data; - payload = g_new0 (ActionPayload, 1); - payload->action = action; - payload->sidebar = sidebar; - item = gtk_menu_item_new_with_mnemonic (nemo_action_get_orig_label (action)); - payload->item = item; - g_signal_connect (item, "activate", G_CALLBACK (action_activated_callback), payload); - gtk_widget_show (item); - gtk_menu_shell_insert (GTK_MENU_SHELL (sidebar->popup_menu), item, index); - sidebar->action_items = g_list_append (sidebar->action_items, payload); - index ++; - } -} - #if GTK_CHECK_VERSION (3, 24, 8) static void moved_to_rect_cb (GdkWindow *window, @@ -3524,172 +3431,11 @@ popup_menu_realized (GtkWidget *menu, } #endif -/* Constructs the popup menu for the file list if needed */ -static void -bookmarks_build_popup_menu (NemoPlacesSidebar *sidebar) -{ - GtkWidget *item; - gboolean use_browser; - - if (sidebar->popup_menu) { - return; - } - - use_browser = g_settings_get_boolean (nemo_preferences, - NEMO_PREFERENCES_ALWAYS_USE_BROWSER); - - sidebar->popup_menu = gtk_menu_new (); - -#if GTK_CHECK_VERSION (3, 24, 8) - g_signal_connect (sidebar->popup_menu, "realize", - G_CALLBACK (popup_menu_realized), - sidebar); - gtk_widget_realize (sidebar->popup_menu); -#endif - - gtk_menu_attach_to_widget (GTK_MENU (sidebar->popup_menu), - GTK_WIDGET (sidebar), - bookmarks_popup_menu_detach_cb); - - item = gtk_image_menu_item_new_with_mnemonic (_("_Open")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), - gtk_image_new_from_icon_name ("folder-open-symbolic", GTK_ICON_SIZE_MENU)); - g_signal_connect (item, "activate", - G_CALLBACK (open_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("Open in New _Tab")); - sidebar->popup_menu_open_in_new_tab_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (open_shortcut_in_new_tab_cb), sidebar); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - if (use_browser) { - gtk_widget_show (item); - } - - item = gtk_menu_item_new_with_mnemonic (_("Open in New _Window")); - g_signal_connect (item, "activate", - G_CALLBACK (open_shortcut_in_new_window_cb), sidebar); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - if (use_browser) { - gtk_widget_show (item); - } - sidebar->popup_menu_remove_rename_separator_item = - GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (sidebar->popup_menu))); - - item = gtk_menu_item_new_with_mnemonic (_("_Add Bookmark")); - sidebar->popup_menu_add_shortcut_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (add_shortcut_cb), sidebar); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_image_menu_item_new_with_label (_("Remove")); - sidebar->popup_menu_remove_item = item; - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), - gtk_image_new_from_icon_name ("list-remove-symbolic", GTK_ICON_SIZE_MENU)); - g_signal_connect (item, "activate", - G_CALLBACK (remove_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("_Rename...")); - sidebar->popup_menu_rename_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (rename_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - /* Nemo Actions */ - sidebar->popup_menu_action_separator_item = - GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (sidebar->popup_menu))); - - /* Mount/Unmount/Eject menu items */ - - sidebar->popup_menu_separator_item = - GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (sidebar->popup_menu))); - - item = gtk_menu_item_new_with_mnemonic (_("_Mount")); - sidebar->popup_menu_mount_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (mount_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("_Unmount")); - sidebar->popup_menu_unmount_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (unmount_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("_Eject")); - sidebar->popup_menu_eject_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (eject_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("_Detect Media")); - sidebar->popup_menu_rescan_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (rescan_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("_Start")); - sidebar->popup_menu_start_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (start_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - item = gtk_menu_item_new_with_mnemonic (_("_Stop")); - sidebar->popup_menu_stop_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (stop_shortcut_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - /* Empty Trash menu item */ - - item = gtk_menu_item_new_with_mnemonic (_("Empty _Trash")); - sidebar->popup_menu_empty_trash_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (empty_trash_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - /* Properties menu item */ - - sidebar->popup_menu_properties_separator_item = - GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (sidebar->popup_menu))); - - item = gtk_menu_item_new_with_mnemonic (_("_Properties")); - sidebar->popup_menu_properties_item = item; - g_signal_connect (item, "activate", - G_CALLBACK (properties_cb), sidebar); - gtk_widget_show (item); - gtk_menu_shell_append (GTK_MENU_SHELL (sidebar->popup_menu), item); - - add_action_popup_items (sidebar); - - bookmarks_check_popup_sensitivity (sidebar); -} - -static void -bookmarks_update_popup_menu (NemoPlacesSidebar *sidebar) -{ - bookmarks_build_popup_menu (sidebar); -} - static void bookmarks_popup_menu (NemoPlacesSidebar *sidebar, GdkEventButton *event) { - bookmarks_update_popup_menu (sidebar); + update_menu_states (sidebar); eel_pop_up_context_menu (GTK_MENU(sidebar->popup_menu), (GdkEvent *) event, GTK_WIDGET (sidebar)); @@ -3705,11 +3451,167 @@ bookmarks_popup_menu_cb (GtkWidget *widget, } static void -actions_changed_callback (NemoPlacesSidebar *sidebar) +reset_menu (NemoPlacesSidebar *sidebar) +{ + sidebar->actions_need_update = TRUE; + rebuild_menu (sidebar); +} + +static gboolean +actions_changed_idle_cb (gpointer user_data) +{ + NemoPlacesSidebar *sidebar = NEMO_PLACES_SIDEBAR (user_data); + + reset_menu (sidebar); + + sidebar->actions_changed_idle_id = 0; + return G_SOURCE_REMOVE; +} + +static void +actions_changed (gpointer user_data) +{ + NemoPlacesSidebar *sidebar = NEMO_PLACES_SIDEBAR (user_data); + + g_clear_handle_id (&sidebar->actions_changed_idle_id, g_source_remove); + sidebar->actions_changed_idle_id = g_idle_add (actions_changed_idle_cb, sidebar); +} + +static void +add_action_to_ui (NemoActionManager *manager, + GtkAction *action, + GtkUIManagerItemType type, + const gchar *path, + gpointer user_data) +{ + NemoPlacesSidebar *sidebar = NEMO_PLACES_SIDEBAR (user_data); + + if (type != GTK_UI_MANAGER_SEPARATOR) { + if (type == GTK_UI_MANAGER_MENUITEM) { + g_signal_handlers_disconnect_by_func (action, + run_action_callback, + sidebar); + + g_signal_connect (action, "activate", + G_CALLBACK (run_action_callback), + sidebar); + } + + gtk_action_group_add_action (sidebar->action_action_group, + action); + gtk_action_set_visible (GTK_ACTION (action), FALSE); + } + + const gchar *placeholder = "/selection/PlacesSidebarActionsPlaceholder"; + + g_autofree gchar *full_path = NULL; + const gchar *name; + + if (path != NULL) { + full_path = g_strdup_printf ("%s/%s", placeholder, path); + } + else { + full_path = g_strdup (placeholder); + } + + if (type == GTK_UI_MANAGER_SEPARATOR) { + name = NULL; + } + else { + name = gtk_action_get_name (action); + } + + gtk_ui_manager_add_ui (sidebar->ui_manager, + sidebar->action_action_group_merge_id, + full_path, + name, + name, + type, + FALSE); +} + +static void +clear_ui (NemoPlacesSidebar *sidebar) +{ + + nemo_ui_unmerge_ui (sidebar->ui_manager, + &sidebar->bookmark_action_group_merge_id, + &sidebar->bookmark_action_group); + + nemo_ui_unmerge_ui (sidebar->ui_manager, + &sidebar->action_action_group_merge_id, + &sidebar->action_action_group); + +} + +static const GtkActionEntry bookmark_action_entries[] = { + { NEMO_ACTION_OPEN, "folder-open-symbolic", N_("_Open"), NULL, NULL, G_CALLBACK (open_shortcut_cb) }, + { NEMO_ACTION_OPEN_IN_NEW_TAB, NULL, N_("Open in New _Tab"), NULL, NULL, G_CALLBACK (open_shortcut_in_new_tab_cb) }, + { NEMO_ACTION_OPEN_ALTERNATE, NULL, N_("Open in New _Window"), NULL, NULL, G_CALLBACK (open_shortcut_in_new_window_cb) }, + { NEMO_ACTION_ADD_BOOKMARK, NULL, N_("_Add Bookmark"), NULL, NULL, G_CALLBACK (add_shortcut_cb) }, + { NEMO_ACTION_SIDEBAR_REMOVE, "list-remove-symbolic", N_("Remove"), NULL, NULL, G_CALLBACK (remove_shortcut_cb) }, + { NEMO_ACTION_RENAME, NULL, N_("_Rename..."), NULL, NULL, G_CALLBACK (rename_shortcut_cb) }, + { NEMO_ACTION_MOUNT_VOLUME, NULL, N_("_Mount"), NULL, NULL, G_CALLBACK (mount_shortcut_cb) }, + { NEMO_ACTION_UNMOUNT_VOLUME, NULL, N_("_Unmount"), NULL, NULL, G_CALLBACK (unmount_shortcut_cb) }, + { NEMO_ACTION_EJECT_VOLUME, NULL, N_("_Eject"), NULL, NULL, G_CALLBACK (eject_shortcut_cb) }, + { NEMO_ACTION_SIDEBAR_DETECT_MEDIA, NULL, N_("_Detect Media"), NULL, NULL, G_CALLBACK (rescan_shortcut_cb) }, + { NEMO_ACTION_START_VOLUME, NULL, N_("_Start"), NULL, NULL, G_CALLBACK (start_shortcut_cb) }, + { NEMO_ACTION_STOP_VOLUME, NULL, N_("_Stop"), NULL, NULL, G_CALLBACK (stop_shortcut_cb) }, + { NEMO_ACTION_EMPTY_TRASH_CONDITIONAL, NULL, N_("_Empty _Trash"), NULL, NULL, G_CALLBACK (empty_trash_cb) }, + { NEMO_ACTION_PROPERTIES, NULL, N_("_Properties"), NULL, NULL, G_CALLBACK (properties_cb) }, +}; + +static void +rebuild_menu (NemoPlacesSidebar *sidebar) { - if (sidebar->popup_menu) { - gtk_menu_detach (GTK_MENU (sidebar->popup_menu)); + if (!gtk_widget_get_realized (GTK_WIDGET (sidebar))) { + return; + } + + if (!sidebar->actions_need_update) { + return; + } + + clear_ui (sidebar); + + nemo_ui_prepare_merge_ui (sidebar->ui_manager, + "NemoPlacesSidebarBookmarkActions", + &sidebar->bookmark_action_group_merge_id, + &sidebar->bookmark_action_group); + + nemo_ui_prepare_merge_ui (sidebar->ui_manager, + "NemoPlacesSidebarActionActions", + &sidebar->action_action_group_merge_id, + &sidebar->action_action_group); + + sidebar->bookmark_action_group_merge_id = + gtk_ui_manager_add_ui_from_resource (sidebar->ui_manager, "/org/nemo/nemo-places-sidebar-ui.xml", NULL); + + gtk_action_group_add_actions (sidebar->bookmark_action_group, + bookmark_action_entries, + G_N_ELEMENTS (bookmark_action_entries), + sidebar); + + nemo_action_manager_iterate_actions (sidebar->action_manager, + (NemoActionManagerIterFunc) add_action_to_ui, + sidebar); + + if (sidebar->popup_menu == NULL) { + GtkWidget *menu = gtk_ui_manager_get_widget (sidebar->ui_manager, "/selection"); + gtk_menu_set_screen (GTK_MENU (menu), gtk_widget_get_screen (GTK_WIDGET (sidebar->window))); + sidebar->popup_menu = menu; + +#if GTK_CHECK_VERSION (3, 24, 8) + g_signal_connect (sidebar->popup_menu, "realize", + G_CALLBACK (popup_menu_realized), + sidebar); + gtk_widget_realize (sidebar->popup_menu); +#endif + + gtk_widget_show (menu); } + + sidebar->actions_need_update = FALSE; } static gboolean @@ -3955,7 +3857,7 @@ trash_state_changed_cb (NemoTrashMonitor *trash_monitor, /* The trash icon changed, update the sidebar */ update_places (sidebar); - bookmarks_check_popup_sensitivity (sidebar); + // reset_menu (sidebar); } static void @@ -4102,12 +4004,11 @@ nemo_places_sidebar_init (NemoPlacesSidebar *sidebar) GtkStyleContext *style_context; sidebar->action_manager = nemo_action_manager_new (); - sidebar->action_manager_changed_id = g_signal_connect_swapped (sidebar->action_manager, - "changed", - G_CALLBACK (actions_changed_callback), - sidebar); - - sidebar->action_items = NULL; + sidebar->actions_changed_id = g_signal_connect_swapped (sidebar->action_manager, + "changed", + G_CALLBACK (actions_changed), + sidebar); + sidebar->ui_manager = gtk_ui_manager_new (); sidebar->in_drag = FALSE; @@ -4406,21 +4307,16 @@ nemo_places_sidebar_dispose (GObject *object) free_drag_data (sidebar); - if (sidebar->action_items != NULL) { - g_list_free_full (sidebar->action_items, g_free); - sidebar->action_items = NULL; - } - if (sidebar->bookmarks_changed_id != 0) { g_signal_handler_disconnect (sidebar->bookmarks, sidebar->bookmarks_changed_id); sidebar->bookmarks_changed_id = 0; } - if (sidebar->action_manager_changed_id != 0) { + if (sidebar->actions_changed_id != 0) { g_signal_handler_disconnect (sidebar->action_manager, - sidebar->action_manager_changed_id); - sidebar->action_manager_changed_id = 0; + sidebar->actions_changed_id); + sidebar->actions_changed_id = 0; } g_clear_object (&sidebar->action_manager); @@ -4449,10 +4345,6 @@ nemo_places_sidebar_dispose (GObject *object) desktop_setting_changed_callback, sidebar); - g_signal_handlers_disconnect_by_func (nemo_preferences, - bookmarks_popup_menu_detach_cb, - sidebar); - g_signal_handlers_disconnect_by_func (gnome_background_preferences, desktop_setting_changed_callback, sidebar); @@ -4580,7 +4472,7 @@ nemo_places_sidebar_set_parent_window (NemoPlacesSidebar *sidebar, G_CALLBACK (drive_changed_callback), sidebar, 0); g_signal_connect_swapped (nemo_preferences, "changed::" NEMO_PREFERENCES_ALWAYS_USE_BROWSER, - G_CALLBACK (bookmarks_popup_menu_detach_cb), sidebar); + G_CALLBACK (reset_menu), sidebar); update_places (sidebar); } diff --git a/src/nemo-tree-sidebar.c b/src/nemo-tree-sidebar.c index a036aa971..79e7fafb0 100644 --- a/src/nemo-tree-sidebar.c +++ b/src/nemo-tree-sidebar.c @@ -33,6 +33,7 @@ #include "nemo-tree-sidebar.h" +#include "nemo-actions.h" #include "nemo-tree-sidebar-model.h" #include "nemo-properties-window.h" #include "nemo-window-slot.h" @@ -50,6 +51,7 @@ #include #include #include +#include #include #include @@ -88,34 +90,25 @@ struct FMTreeViewDetails { guint show_selection_idle_id; gulong clipboard_handler_id; - GtkWidget *popup; - GtkWidget *popup_open; - GtkWidget *popup_open_in_new_window; - GtkWidget *popup_open_in_new_tab; - GtkWidget *popup_create_folder; - GtkWidget *popup_post_create_folder_separator; - GtkWidget *popup_cut; - GtkWidget *popup_copy; - GtkWidget *popup_paste; - GtkWidget *popup_rename; - GtkWidget *popup_pin; - GtkWidget *popup_post_pin_separator; - GtkWidget *popup_unpin; - GtkWidget *popup_trash; - GtkWidget *popup_delete; - GtkWidget *popup_properties; - GtkWidget *popup_unmount_separator; - GtkWidget *popup_unmount; - GtkWidget *popup_eject; - GtkWidget *popup_action_separator; + GtkWidget *popup_menu; + NemoFile *popup_file; guint popup_file_idle_handler; guint selection_changed_timer; NemoActionManager *action_manager; - guint action_manager_changed_id; - GList *action_items; + gulong actions_changed_id; + guint actions_changed_idle_id; + + GtkUIManager *ui_manager; + GtkActionGroup *tv_action_group; + guint tv_action_group_merge_id; + GtkActionGroup *action_action_group; + guint action_action_group_merge_id; + + gboolean actions_need_update; + guint hidden_files_changed_id; guint sort_directories_first : 1; guint sort_favorites_first : 1; @@ -126,21 +119,17 @@ typedef struct { FMTreeView *view; } PrependURIParameters; -typedef struct { - NemoAction *action; - FMTreeView *view; - GtkWidget *item; -} ActionPayload; - static GdkAtom copied_files_atom; static void fm_tree_view_activate_file (FMTreeView *view, NemoFile *file, NemoWindowOpenFlags flags); -static void create_popup_menu (FMTreeView *view); +static void popup_menu (FMTreeView *view, GdkEventButton *event); +static void rebuild_menu (FMTreeView *view); +// static void create_popup_menu (FMTreeView *view); -static void add_action_popup_items (FMTreeView *view); +// static void add_action_popup_items (FMTreeView *view); G_DEFINE_TYPE (FMTreeView, fm_tree_view, GTK_TYPE_SCROLLED_WINDOW) #define parent_class fm_tree_view_parent_class @@ -653,24 +642,6 @@ mount_removed_callback (GVolumeMonitor *volume_monitor, g_free (mount_uri); } -static void -clipboard_contents_received_callback (GtkClipboard *clipboard, - GtkSelectionData *selection_data, - gpointer data) -{ - FMTreeView *view; - - view = FM_TREE_VIEW (data); - - if (gtk_selection_data_get_data_type (selection_data) == copied_files_atom - && gtk_selection_data_get_length (selection_data) > 0 && - view->details->popup != NULL) { - gtk_widget_set_sensitive (view->details->popup_paste, TRUE); - } - - g_object_unref (view); -} - static gboolean is_parent_writable (NemoFile *file) { @@ -690,198 +661,189 @@ is_parent_writable (NemoFile *file) return result; } -static gboolean -button_pressed_callback (GtkTreeView *treeview, GdkEventButton *event, - FMTreeView *view) +static void +set_action_sensitive (GtkActionGroup *action_group, + const gchar *name, + gboolean sensitive) { - GtkTreePath *path; - gboolean parent_file_is_writable; - gboolean file_is_home_or_desktop; - gboolean file_is_special_link; - gboolean can_move_file_to_trash; - gboolean can_delete_file; - gboolean using_browser; - gint is_toplevel; + GtkAction *action; - using_browser = g_settings_get_boolean (nemo_preferences, - NEMO_PREFERENCES_ALWAYS_USE_BROWSER); + action = gtk_action_group_get_action (action_group, name); + gtk_action_set_sensitive (action, sensitive); +} - if (event->button == 3) { - gboolean show_unmount = FALSE; - gboolean show_eject = FALSE; - GMount *mount = NULL; +static void +set_action_visible (GtkActionGroup *action_group, + const gchar *name, + gboolean visible) +{ + GtkAction *action; - if (view->details->popup_file != NULL) { - return FALSE; /* Already up, ignore */ - } - - if (!gtk_tree_view_get_path_at_pos (treeview, event->x, event->y, - &path, NULL, NULL, NULL)) { - return FALSE; - } + action = gtk_action_group_get_action (action_group, name); + gtk_action_set_visible (action, visible); +} - view->details->popup_file = sort_model_path_to_file (view, path); - if (view->details->popup_file == NULL) { - gtk_tree_path_free (path); - return FALSE; - } +static void +clipboard_contents_received_callback (GtkClipboard *clipboard, + GtkSelectionData *selection_data, + gpointer data) +{ + FMTreeView *view; - create_popup_menu (view); + view = FM_TREE_VIEW (data); - is_toplevel = gtk_tree_path_get_depth (path) == 1; + if (gtk_selection_data_get_data_type (selection_data) == copied_files_atom + && gtk_selection_data_get_length (selection_data) > 0) { + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_PASTE, TRUE); + } - if (using_browser) { - gtk_widget_set_sensitive (view->details->popup_open_in_new_window, - nemo_file_is_directory (view->details->popup_file)); - gtk_widget_set_sensitive (view->details->popup_open_in_new_tab, - nemo_file_is_directory (view->details->popup_file)); - } + g_object_unref (view); +} - gtk_widget_set_sensitive (view->details->popup_create_folder, - nemo_file_is_directory (view->details->popup_file) && - nemo_file_can_write (view->details->popup_file)); +static void +update_menu_states (FMTreeView *view, + GdkEventButton *event) +{ + GtkTreePath *path; + gboolean parent_file_is_writable; + gboolean file_is_home_or_desktop; + gboolean file_is_special_link; + gboolean can_move_file_to_trash; + gboolean can_delete_file; + gboolean using_browser; + gboolean is_dir; + gboolean can_write; + gint is_toplevel; - if (nemo_file_is_in_favorites (view->details->popup_file)) { - gtk_widget_hide (view->details->popup_create_folder); - gtk_widget_hide (view->details->popup_post_create_folder_separator); - } else { - gtk_widget_show (view->details->popup_create_folder); - gtk_widget_show (view->details->popup_post_create_folder_separator); - } + using_browser = g_settings_get_boolean (nemo_preferences, + NEMO_PREFERENCES_ALWAYS_USE_BROWSER); - gtk_widget_set_sensitive (view->details->popup_paste, FALSE); - if (nemo_file_is_directory (view->details->popup_file) && - nemo_file_can_write (view->details->popup_file)) { - gtk_clipboard_request_contents (nemo_clipboard_get (GTK_WIDGET (view->details->tree_widget)), - copied_files_atom, - clipboard_contents_received_callback, g_object_ref (view)); - } - can_move_file_to_trash = nemo_file_can_trash (view->details->popup_file); - gtk_widget_set_sensitive (view->details->popup_trash, can_move_file_to_trash); - - if (g_settings_get_boolean (nemo_preferences, NEMO_PREFERENCES_ENABLE_DELETE)) { - parent_file_is_writable = is_parent_writable (view->details->popup_file); - file_is_home_or_desktop = nemo_file_is_home (view->details->popup_file) - || nemo_file_is_desktop_directory (view->details->popup_file); - file_is_special_link = NEMO_IS_DESKTOP_ICON_FILE (view->details->popup_file); - - can_delete_file = parent_file_is_writable - && !file_is_home_or_desktop - && !file_is_special_link; - - gtk_widget_show (view->details->popup_delete); - gtk_widget_set_sensitive (view->details->popup_delete, can_delete_file); - } else { - gtk_widget_hide (view->details->popup_delete); - } + gboolean show_unmount = FALSE; + gboolean show_eject = FALSE; + GMount *mount = NULL; - mount = fm_tree_model_get_mount_for_root_node_file (view->details->child_model, view->details->popup_file); - if (mount) { - show_unmount = g_mount_can_unmount (mount); - show_eject = g_mount_can_eject (mount); - } + if (!gtk_tree_view_get_path_at_pos (view->details->tree_widget, event->x, event->y, + &path, NULL, NULL, NULL)) { + return; + } - if (show_unmount) { - gtk_widget_show (view->details->popup_unmount); - } else { - gtk_widget_hide (view->details->popup_unmount); - } + NemoFile *file = sort_model_path_to_file (view, path); + view->details->popup_file = nemo_file_ref (file); - if (show_eject) { - gtk_widget_show (view->details->popup_eject); - } else { - gtk_widget_hide (view->details->popup_eject); - } + NemoFile *parent = nemo_file_get_parent (file); + GList *selected = g_list_prepend (NULL, file); + + nemo_action_manager_update_action_states (view->details->action_manager, + view->details->action_action_group, + selected, + parent, + FALSE, + GTK_WINDOW (view->details->window)); + nemo_file_list_free (selected); + nemo_file_unref (parent); + + is_dir = nemo_file_is_directory (file); + can_write = nemo_file_can_write (file); + + is_toplevel = gtk_tree_path_get_depth (path) == 1; + gtk_tree_path_free (path); + + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_OPEN_ALTERNATE, is_dir); + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_OPEN_IN_NEW_TAB, is_dir); + set_action_visible (view->details->tv_action_group, NEMO_ACTION_OPEN_ALTERNATE, using_browser); + set_action_visible (view->details->tv_action_group, NEMO_ACTION_OPEN_IN_NEW_TAB, using_browser); + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_NEW_FOLDER, is_dir && can_write); + set_action_visible (view->details->tv_action_group, NEMO_ACTION_NEW_FOLDER, !nemo_file_is_in_favorites (file)); + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_PASTE, FALSE); + + if (is_dir && can_write) { + gtk_clipboard_request_contents (nemo_clipboard_get (GTK_WIDGET (view->details->tree_widget)), + copied_files_atom, + clipboard_contents_received_callback, g_object_ref (view)); + } - if (show_unmount || show_eject) { - gtk_widget_show (view->details->popup_unmount_separator); - } else { - gtk_widget_hide (view->details->popup_unmount_separator); - } + can_move_file_to_trash = nemo_file_can_trash (file); + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_TRASH, can_move_file_to_trash); + + if (g_settings_get_boolean (nemo_preferences, NEMO_PREFERENCES_ENABLE_DELETE)) { + parent_file_is_writable = is_parent_writable (file); + file_is_home_or_desktop = nemo_file_is_home (file) + || nemo_file_is_desktop_directory (file); + file_is_special_link = NEMO_IS_DESKTOP_ICON_FILE (file); + + can_delete_file = parent_file_is_writable + && !file_is_home_or_desktop + && !file_is_special_link; + + set_action_visible (view->details->tv_action_group, NEMO_ACTION_DELETE, TRUE); + set_action_sensitive (view->details->tv_action_group, NEMO_ACTION_DELETE, can_delete_file); + } else { + set_action_visible (view->details->tv_action_group, NEMO_ACTION_DELETE, FALSE); + } - if (!is_toplevel && !nemo_file_is_in_favorites (view->details->popup_file)) { - if (nemo_file_get_pinning (view->details->popup_file)) { - gtk_widget_hide (view->details->popup_pin); - gtk_widget_show (view->details->popup_unpin); - } else { - gtk_widget_show (view->details->popup_pin); - gtk_widget_hide (view->details->popup_unpin); - } + mount = fm_tree_model_get_mount_for_root_node_file (view->details->child_model, file); + if (mount) { + show_unmount = g_mount_can_unmount (mount); + show_eject = g_mount_can_eject (mount); + } - gtk_widget_show (view->details->popup_post_pin_separator); - } else { - gtk_widget_hide (view->details->popup_pin); - gtk_widget_hide (view->details->popup_unpin); - gtk_widget_hide (view->details->popup_post_pin_separator); - } + set_action_visible (view->details->tv_action_group, NEMO_ACTION_UNMOUNT_VOLUME, show_unmount); + set_action_visible (view->details->tv_action_group, NEMO_ACTION_EJECT_VOLUME, show_eject); + + if (!is_toplevel && !nemo_file_is_in_favorites (file)) { + set_action_visible (view->details->tv_action_group, NEMO_ACTION_PIN_FILE, !nemo_file_get_pinning (file)); + set_action_visible (view->details->tv_action_group, NEMO_ACTION_UNPIN_FILE, nemo_file_get_pinning (file)); + } else { + set_action_visible (view->details->tv_action_group, NEMO_ACTION_PIN_FILE, FALSE); + set_action_visible (view->details->tv_action_group, NEMO_ACTION_UNPIN_FILE, FALSE); + } +} - gboolean actions_visible = FALSE; - - GList *l; - NemoFile *file = view->details->popup_file; - NemoFile *parent = nemo_file_get_parent (file); - GList *tmp = NULL; - tmp = g_list_append (tmp, file); - ActionPayload *p; - - for (l = view->details->action_items; l != NULL; l = l->next) { - p = l->data; - if (nemo_action_get_visibility (p->action, tmp, parent, FALSE, GTK_WINDOW (view->details->window))) { - gchar *action_label; - - action_label = nemo_action_get_label (p->action, tmp, parent, GTK_WINDOW (view->details->window)); - gtk_menu_item_set_label (GTK_MENU_ITEM (p->item), action_label); - g_free (action_label); - - gtk_widget_set_visible (p->item, TRUE); - actions_visible = TRUE; - } else { - gtk_widget_set_visible (p->item, FALSE); - } +static gboolean +button_pressed_callback (GtkTreeView *treeview, + GdkEventButton *event, + FMTreeView *view) +{ + g_return_val_if_fail (FM_IS_TREE_VIEW (view), GDK_EVENT_PROPAGATE); + + if (event->button == 3) { + popup_menu (view, event); + return GDK_EVENT_STOP; + } else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) { + NemoFile *file; + GtkTreePath *path; + gboolean using_browser; + NemoWindowOpenFlags flags = 0; + + using_browser = g_settings_get_boolean (nemo_preferences, + NEMO_PREFERENCES_ALWAYS_USE_BROWSER); + + if (!gtk_tree_view_get_path_at_pos (treeview, event->x, event->y, + &path, NULL, NULL, NULL)) { + return FALSE; } - gtk_widget_set_visible (view->details->popup_action_separator, actions_visible); + file = sort_model_path_to_file (view, path); - g_list_free (tmp); // Don't free the file, just the list, the file is owned by the model. + if (using_browser) { + flags = (event->state & GDK_CONTROL_MASK) ? + NEMO_WINDOW_OPEN_FLAG_NEW_WINDOW : + NEMO_WINDOW_OPEN_FLAG_NEW_TAB; + } else { + flags = NEMO_WINDOW_OPEN_FLAG_CLOSE_BEHIND; + } - gtk_menu_popup (GTK_MENU (view->details->popup), - NULL, NULL, NULL, NULL, - event->button, event->time); + if (file) { + fm_tree_view_activate_file (view, file, flags); + nemo_file_unref (file); + } - gtk_tree_view_set_cursor (view->details->tree_widget, path, NULL, FALSE); gtk_tree_path_free (path); - return FALSE; - } else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) { - NemoFile *file; - NemoWindowOpenFlags flags = 0; - - if (!gtk_tree_view_get_path_at_pos (treeview, event->x, event->y, - &path, NULL, NULL, NULL)) { - return FALSE; - } - - file = sort_model_path_to_file (view, path); - - if (using_browser) { - flags = (event->state & GDK_CONTROL_MASK) ? - NEMO_WINDOW_OPEN_FLAG_NEW_WINDOW : - NEMO_WINDOW_OPEN_FLAG_NEW_TAB; - } else { - flags = NEMO_WINDOW_OPEN_FLAG_CLOSE_BEHIND; - } - - if (file) { - fm_tree_view_activate_file (view, file, flags); - nemo_file_unref (file); - } - - gtk_tree_path_free (path); - - return TRUE; - } + return GDK_EVENT_STOP; + } - return FALSE; + return GDK_EVENT_PROPAGATE; } static gboolean @@ -920,21 +882,21 @@ fm_tree_view_activate_file (FMTreeView *view, } static void -fm_tree_view_open_cb (GtkWidget *menu_item, +fm_tree_view_open_cb (GtkAction *action, FMTreeView *view) { fm_tree_view_activate_file (view, view->details->popup_file, 0); } static void -fm_tree_view_open_in_new_tab_cb (GtkWidget *menu_item, +fm_tree_view_open_in_new_tab_cb (GtkAction *action, FMTreeView *view) { fm_tree_view_activate_file (view, view->details->popup_file, NEMO_WINDOW_OPEN_FLAG_NEW_TAB); } static void -fm_tree_view_open_in_new_window_cb (GtkWidget *menu_item, +fm_tree_view_open_in_new_window_cb (GtkAction *action, FMTreeView *view) { fm_tree_view_activate_file (view, view->details->popup_file, NEMO_WINDOW_OPEN_FLAG_NEW_WINDOW); @@ -958,7 +920,7 @@ new_folder_done (GFile *new_folder, } static void -fm_tree_view_create_folder_cb (GtkWidget *menu_item, +fm_tree_view_create_folder_cb (GtkAction *action, FMTreeView *view) { char *parent_uri; @@ -1025,14 +987,14 @@ copy_or_cut_files (FMTreeView *view, } static void -fm_tree_view_cut_cb (GtkWidget *menu_item, +fm_tree_view_cut_cb (GtkAction *action, FMTreeView *view) { copy_or_cut_files (view, TRUE); } static void -fm_tree_view_copy_cb (GtkWidget *menu_item, +fm_tree_view_copy_cb (GtkAction *action, FMTreeView *view) { copy_or_cut_files (view, FALSE); @@ -1087,7 +1049,7 @@ paste_into_clipboard_received_callback (GtkClipboard *clipboard, } static void -fm_tree_view_paste_cb (GtkWidget *menu_item, +fm_tree_view_paste_cb (GtkAction *action, FMTreeView *view) { gtk_clipboard_request_contents (nemo_clipboard_get (GTK_WIDGET (view->details->tree_widget)), @@ -1111,7 +1073,7 @@ fm_tree_view_get_containing_window (FMTreeView *view) } static void -fm_tree_view_pin_unpin_cb (GtkWidget *menu_item, +fm_tree_view_pin_unpin_cb (GtkAction *action, FMTreeView *view) { nemo_file_set_pinning (view->details->popup_file, @@ -1119,7 +1081,7 @@ fm_tree_view_pin_unpin_cb (GtkWidget *menu_item, } static void -fm_tree_view_trash_cb (GtkWidget *menu_item, +fm_tree_view_trash_cb (GtkAction *action, FMTreeView *view) { GList *list; @@ -1138,7 +1100,7 @@ fm_tree_view_trash_cb (GtkWidget *menu_item, } static void -fm_tree_view_delete_cb (GtkWidget *menu_item, +fm_tree_view_delete_cb (GtkAction *action, FMTreeView *view) { GList *location_list; @@ -1155,7 +1117,7 @@ fm_tree_view_delete_cb (GtkWidget *menu_item, } static void -fm_tree_view_properties_cb (GtkWidget *menu_item, +fm_tree_view_properties_cb (GtkAction *action, FMTreeView *view) { GList *list; @@ -1168,7 +1130,7 @@ fm_tree_view_properties_cb (GtkWidget *menu_item, } static void -fm_tree_view_unmount_cb (GtkWidget *menu_item, +fm_tree_view_unmount_cb (GtkAction *action, FMTreeView *view) { NemoFile *file = view->details->popup_file; @@ -1187,7 +1149,7 @@ fm_tree_view_unmount_cb (GtkWidget *menu_item, } static void -fm_tree_view_eject_cb (GtkWidget *menu_item, +fm_tree_view_eject_cb (GtkAction *action, FMTreeView *view) { NemoFile *file = view->details->popup_file; @@ -1239,305 +1201,246 @@ popup_menu_deactivated (GtkMenuShell *menu_shell, gpointer data) } static void -action_activated_callback (GtkMenuItem *item, ActionPayload *payload) +run_action_callback (GtkAction *action, gpointer user_data) { - gchar *uri = NULL; + FMTreeView *view = FM_TREE_VIEW (user_data); + g_return_if_fail (view->details->popup_file != NULL); - FMTreeView *view = payload->view; + g_autofree gchar *uri = NULL; - NemoFile *file = view->details->popup_file; + uri = nemo_file_get_uri (view->details->popup_file); + + if (!uri) { + return; + } + + NemoFile *file = nemo_file_get_by_uri (uri); NemoFile *parent = nemo_file_get_parent (file); - GList *tmp = NULL; - tmp = g_list_append (tmp, file); + GList *selection = g_list_prepend (NULL, file); + + nemo_action_activate (NEMO_ACTION (action), selection, parent, GTK_WINDOW (view->details->window)); + + nemo_file_list_free (selection); + nemo_file_unref (parent); +} + +#if GTK_CHECK_VERSION (3, 24, 8) +static void +moved_to_rect_cb (GdkWindow *window, + const GdkRectangle *flipped_rect, + const GdkRectangle *final_rect, + gboolean flipped_x, + gboolean flipped_y, + GtkMenu *menu) +{ + g_signal_emit_by_name (menu, + "popped-up", + 0, + flipped_rect, + final_rect, + flipped_x, + flipped_y); + + // Don't let the emission run in gtkmenu.c + g_signal_stop_emission_by_name (window, "moved-to-rect"); +} - nemo_action_activate (NEMO_ACTION (payload->action), tmp, parent, GTK_WINDOW (view->details->window)); +static void +popup_menu_realized (GtkWidget *menu, + gpointer user_data) +{ + GdkWindow *toplevel; + + toplevel = gtk_widget_get_window (gtk_widget_get_toplevel (menu)); - nemo_file_list_free (tmp); + g_signal_handlers_disconnect_by_func (toplevel, moved_to_rect_cb, menu); - g_free (uri); + g_signal_connect (toplevel, "moved-to-rect", G_CALLBACK (moved_to_rect_cb), + menu); } +#endif static void -add_action_popup_items (FMTreeView *view) -{ - if (view->details->action_items != NULL) - g_list_free_full (view->details->action_items, g_free); - - view->details->action_items = NULL; - - GList *action_list = nemo_action_manager_list_actions (view->details->action_manager); - GList *l; - GtkWidget *menu_item; - NemoAction *action; - ActionPayload *payload; - - gint index = 13; - - for (l = action_list; l != NULL; l = l->next) { - action = l->data; - payload = g_new0 (ActionPayload, 1); - payload->action = action; - payload->view = view; - menu_item = gtk_menu_item_new_with_mnemonic (nemo_action_get_orig_label (action)); - payload->item = menu_item; - g_signal_connect (menu_item, "activate", G_CALLBACK (action_activated_callback), payload); - gtk_widget_show (menu_item); - gtk_menu_shell_insert (GTK_MENU_SHELL (view->details->popup), menu_item, index); - view->details->action_items = g_list_append (view->details->action_items, payload); - index ++; - } +popup_menu (FMTreeView *view, + GdkEventButton *event) +{ + update_menu_states (view, event); + eel_pop_up_context_menu (GTK_MENU (view->details->popup_menu), + (GdkEvent *) event, + GTK_WIDGET (view->details->tree_widget)); +} + +/* Callback used for the GtkWidget::popup-menu signal of the shortcuts list */ +static gboolean +popup_menu_cb (GtkAction *widget, + FMTreeView *view) +{ + popup_menu (view, NULL); + return TRUE; } -/* Callback used when the file list's popup menu is detached */ static void -popup_menu_detach_cb (GtkWidget *attach_widget, - GtkMenu *menu) +reset_menu (FMTreeView *view) { - FMTreeView *view; + view->details->actions_need_update = TRUE; + rebuild_menu (view); +} - view = FM_TREE_VIEW (attach_widget); - g_assert (FM_IS_TREE_VIEW (view)); - - view->details->popup = NULL; - view->details->popup_open = NULL; - view->details->popup_open_in_new_window = NULL; - view->details->popup_open_in_new_tab = NULL; - view->details->popup_create_folder = NULL; - view->details->popup_cut = NULL; - view->details->popup_copy = NULL; - view->details->popup_paste = NULL; - view->details->popup_rename = NULL; - view->details->popup_pin = NULL; - view->details->popup_trash = NULL; - view->details->popup_delete = NULL; - view->details->popup_properties = NULL; - view->details->popup_unmount_separator = NULL; - view->details->popup_unmount = NULL; - view->details->popup_eject = NULL; - view->details->popup_action_separator = NULL; +static gboolean +actions_changed_idle_cb (gpointer user_data) +{ + FMTreeView *view = FM_TREE_VIEW (user_data); + + reset_menu (view); + + view->details->actions_changed_idle_id = 0; + return G_SOURCE_REMOVE; } static void -actions_changed_callback (FMTreeView *view) +actions_changed (gpointer user_data) { - if (view->details->popup) { - gtk_menu_detach (GTK_MENU (view->details->popup)); - } + FMTreeView *view = FM_TREE_VIEW (user_data); + + g_clear_handle_id (&view->details->actions_changed_idle_id, g_source_remove); + view->details->actions_changed_idle_id = g_idle_add (actions_changed_idle_cb, view); } static void -create_popup_menu (FMTreeView *view) +add_action_to_ui (NemoActionManager *manager, + GtkAction *action, + GtkUIManagerItemType type, + const gchar *path, + gpointer user_data) { - GtkWidget *popup, *menu_item, *menu_image; + FMTreeView *view = FM_TREE_VIEW (user_data); - if (view->details->popup != NULL) { - /* already created */ - return; - } - - popup = gtk_menu_new (); + if (type != GTK_UI_MANAGER_SEPARATOR) { + if (type == GTK_UI_MANAGER_MENUITEM) { + g_signal_handlers_disconnect_by_func (action, + run_action_callback, + view); - gtk_menu_attach_to_widget (GTK_MENU (popup), - GTK_WIDGET (view), - popup_menu_detach_cb); + g_signal_connect (action, "activate", + G_CALLBACK (run_action_callback), + view); + } - g_signal_connect (popup, "deactivate", - G_CALLBACK (popup_menu_deactivated), - view); + gtk_action_group_add_action (view->details->action_action_group, + action); + gtk_action_set_visible (GTK_ACTION (action), FALSE); + } - /* add the "open" menu item */ - menu_image = gtk_image_new_from_icon_name ("folder-open-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("_Open")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), - menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_open_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_open = menu_item; - - /* add the "open in new tab" menu item */ - menu_item = gtk_menu_item_new_with_mnemonic (_("Open in New _Tab")); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_open_in_new_tab_cb), - view); - g_settings_bind (nemo_preferences, - NEMO_PREFERENCES_ALWAYS_USE_BROWSER, - menu_item, - "visible", - G_SETTINGS_BIND_GET); - - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_open_in_new_tab = menu_item; - - /* add the "open in new window" menu item */ - menu_item = gtk_menu_item_new_with_mnemonic (_("Open in New _Window")); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_open_in_new_window_cb), - view); - g_settings_bind (nemo_preferences, - NEMO_PREFERENCES_ALWAYS_USE_BROWSER, - menu_item, - "visible", - G_SETTINGS_BIND_GET); - - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_open_in_new_window = menu_item; - - eel_gtk_menu_append_separator (GTK_MENU (popup)); + const gchar *placeholder = "/selection/TreeSidebarActionsPlaceholder"; - /* add the "create new folder" menu item */ - menu_item = gtk_image_menu_item_new_with_mnemonic (_("Create New _Folder")); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_create_folder_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_create_folder = menu_item; - - view->details->popup_post_create_folder_separator = GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (popup))); - - /* add the "cut folder" menu item */ - menu_image = gtk_image_new_from_icon_name ("edit-cut-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("Cu_t")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_cut_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_cut = menu_item; - - /* add the "copy folder" menu item */ - menu_image = gtk_image_new_from_icon_name ("edit-copy-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("_Copy")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_copy_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_copy = menu_item; - - /* add the "paste files into folder" menu item */ - menu_image = gtk_image_new_from_icon_name ("edit-paste-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("_Paste Into Folder")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), - menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_paste_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_paste = menu_item; - - eel_gtk_menu_append_separator (GTK_MENU (popup)); - - menu_image = gtk_image_new_from_icon_name ("xapp-pin-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("P_in")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), - menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_pin_unpin_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_pin = menu_item; - - menu_image = gtk_image_new_from_icon_name ("xapp-unpin-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("Unp_in")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), - menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_pin_unpin_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_unpin = menu_item; - - view->details->popup_post_pin_separator = GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (popup))); - - /* add the "move to trash" menu item */ - menu_image = gtk_image_new_from_icon_name (NEMO_ICON_SYMBOLIC_TRASH_FULL, - GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("Mo_ve to Trash")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), - menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_trash_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_trash = menu_item; - - /* add the "delete" menu item */ - menu_image = gtk_image_new_from_icon_name (NEMO_ICON_DELETE, - GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("_Delete")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), - menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_delete_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_delete = menu_item; + g_autofree gchar *full_path = NULL; + const gchar *name; - /* Nemo Actions */ + if (path != NULL) { + full_path = g_strdup_printf ("%s/%s", placeholder, path); + } + else { + full_path = g_strdup (placeholder); + } - view->details->popup_action_separator = - GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (popup))); + if (type == GTK_UI_MANAGER_SEPARATOR) { + name = NULL; + } + else { + name = gtk_action_get_name (action); + } - eel_gtk_menu_append_separator (GTK_MENU (popup)); + gtk_ui_manager_add_ui (view->details->ui_manager, + view->details->action_action_group_merge_id, + full_path, + name, + name, + type, + FALSE); +} - /* add the "Unmount" menu item */ - menu_item = gtk_image_menu_item_new_with_mnemonic (_("_Unmount")); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_unmount_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_unmount = menu_item; - - /* add the "Eject" menu item */ - menu_item = gtk_image_menu_item_new_with_mnemonic (_("_Eject")); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_eject_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_eject = menu_item; - - /* add the unmount separator menu item */ - view->details->popup_unmount_separator = - GTK_WIDGET (eel_gtk_menu_append_separator (GTK_MENU (popup))); - - /* add the "properties" menu item */ - menu_image = gtk_image_new_from_icon_name ("document-properties-symbolic", GTK_ICON_SIZE_MENU); - gtk_widget_show (menu_image); - menu_item = gtk_image_menu_item_new_with_mnemonic (_("P_roperties")); - gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), menu_image); - g_signal_connect (menu_item, "activate", - G_CALLBACK (fm_tree_view_properties_cb), - view); - gtk_widget_show (menu_item); - gtk_menu_shell_append (GTK_MENU_SHELL (popup), menu_item); - view->details->popup_properties = menu_item; +static void +clear_ui (FMTreeView *view) +{ + nemo_ui_unmerge_ui (view->details->ui_manager, + &view->details->tv_action_group_merge_id, + &view->details->tv_action_group); + + nemo_ui_unmerge_ui (view->details->ui_manager, + &view->details->action_action_group_merge_id, + &view->details->action_action_group); +} + +static const GtkActionEntry tree_sidebar_menu_entries[] = { + { NEMO_ACTION_OPEN, "folder-open-symbolic", N_("_Open"), NULL, NULL, G_CALLBACK (fm_tree_view_open_cb) }, + { NEMO_ACTION_OPEN_IN_NEW_TAB, NULL, N_("Open in New _Tab"), NULL, NULL, G_CALLBACK (fm_tree_view_open_in_new_tab_cb) }, + { NEMO_ACTION_OPEN_ALTERNATE, NULL, N_("Open in New _Window"), NULL, NULL, G_CALLBACK (fm_tree_view_open_in_new_window_cb) }, + { NEMO_ACTION_NEW_FOLDER, NULL, N_("Create New _Folder"), NULL, NULL, G_CALLBACK (fm_tree_view_create_folder_cb) }, + { NEMO_ACTION_CUT, "edit-cut-symbolic", N_("Cu_t"), NULL, NULL, G_CALLBACK (fm_tree_view_cut_cb) }, + { NEMO_ACTION_COPY, "edit-copy-symbolic", N_("_Copy"), NULL, NULL, G_CALLBACK (fm_tree_view_copy_cb) }, + { NEMO_ACTION_PASTE, "edit-paste-symbolic", N_("_Paste Into Folder"), NULL, NULL, G_CALLBACK (fm_tree_view_paste_cb) }, + { NEMO_ACTION_PIN_FILE, "xapp-pin-symbolic", N_("P_in"), NULL, NULL, G_CALLBACK (fm_tree_view_pin_unpin_cb) }, + { NEMO_ACTION_UNPIN_FILE, "xapp-unpin-symbolic", N_("Unp_in"), NULL, NULL, G_CALLBACK (fm_tree_view_pin_unpin_cb) }, + { NEMO_ACTION_TRASH, "user-trash-full-symbolic", N_("Mo_ve to Trash"), NULL, NULL, G_CALLBACK (fm_tree_view_trash_cb) }, + { NEMO_ACTION_DELETE, "edit-delete-symbolic", N_("_Delete"), NULL, NULL, G_CALLBACK (fm_tree_view_delete_cb) }, + { NEMO_ACTION_UNMOUNT_VOLUME, NULL, N_("_Unmount"), NULL, NULL, G_CALLBACK (fm_tree_view_unmount_cb) }, + { NEMO_ACTION_EJECT_VOLUME, NULL, N_("_Eject"), NULL, NULL, G_CALLBACK (fm_tree_view_eject_cb) }, + { NEMO_ACTION_PROPERTIES, "document-properties-symbolic", N_("_Properties"), NULL, NULL, G_CALLBACK (fm_tree_view_properties_cb) }, +}; + +static void +rebuild_menu (FMTreeView *view) +{ + if (!view->details->actions_need_update) { + return; + } + + clear_ui (view); + + nemo_ui_prepare_merge_ui (view->details->ui_manager, + "NemoTreeviewSidebarFileActions", + &view->details->tv_action_group_merge_id, + &view->details->tv_action_group); + + nemo_ui_prepare_merge_ui (view->details->ui_manager, + "NemoTreeviewSidebarActionActions", + &view->details->action_action_group_merge_id, + &view->details->action_action_group); + + view->details->tv_action_group_merge_id = + gtk_ui_manager_add_ui_from_resource (view->details->ui_manager, "/org/nemo/nemo-tree-sidebar-ui.xml", NULL); + + nemo_action_manager_iterate_actions (view->details->action_manager, + (NemoActionManagerIterFunc) add_action_to_ui, + view); + + gtk_action_group_add_actions (view->details->tv_action_group, + tree_sidebar_menu_entries, + G_N_ELEMENTS (tree_sidebar_menu_entries), + view); - view->details->popup = popup; + if (view->details->popup_menu == NULL) { + GtkWidget *menu = gtk_ui_manager_get_widget (view->details->ui_manager, "/selection"); + gtk_menu_set_screen (GTK_MENU (menu), gtk_widget_get_screen (GTK_WIDGET (view->details->window))); + view->details->popup_menu = menu; + g_signal_connect (view->details->popup_menu, "deactivate", + G_CALLBACK (popup_menu_deactivated), + view); + +#if GTK_CHECK_VERSION (3, 24, 8) + g_signal_connect (view->details->popup_menu, "realize", + G_CALLBACK (popup_menu_realized), + view->details); + gtk_widget_realize (view->details->popup_menu); +#endif - add_action_popup_items (view); + gtk_widget_show (menu); + } + + view->details->actions_need_update = FALSE; } + static gint get_icon_scale_callback (FMTreeModel *model, FMTreeView *view) @@ -1590,6 +1493,8 @@ create_tree (FMTreeView *view) g_object_unref (view->details->sort_model); + g_signal_connect (view->details->tree_widget, "popup-menu", G_CALLBACK (popup_menu_cb), view); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (view->details->tree_widget)), "NemoSidebar"); @@ -1659,15 +1564,6 @@ create_tree (FMTreeView *view) G_CALLBACK (move_copy_items_callback), view, 0); - view->details->action_manager = nemo_action_manager_new (); - - view->details->action_manager_changed_id = g_signal_connect_swapped (view->details->action_manager, - "changed", - G_CALLBACK (actions_changed_callback), - view); - - view->details->action_items = NULL; - /* Create column */ column = gtk_tree_view_column_new (); @@ -1716,6 +1612,8 @@ create_tree (FMTreeView *view) location = nemo_window_slot_get_current_uri (slot); schedule_select_and_show_location (view, location); g_free (location); + + reset_menu (view); } static void @@ -1832,6 +1730,15 @@ fm_tree_view_init (FMTreeView *view) view->details->selecting = FALSE; + + view->details->action_manager = nemo_action_manager_new (); + + view->details->actions_changed_id = g_signal_connect_swapped (view->details->action_manager, + "changed", + G_CALLBACK (actions_changed), + view); + view->details->ui_manager = gtk_ui_manager_new (); + g_signal_connect_swapped (nemo_tree_sidebar_preferences, "changed::" NEMO_PREFERENCES_TREE_SHOW_ONLY_DIRECTORIES, G_CALLBACK (filtering_changed_callback), view); @@ -1894,11 +1801,6 @@ fm_tree_view_dispose (GObject *object) cancel_activation (view); - if (view->details->popup != NULL) { - gtk_widget_destroy (view->details->popup); - view->details->popup = NULL; - } - if (view->details->popup_file_idle_handler != 0) { g_source_remove (view->details->popup_file_idle_handler); view->details->popup_file_idle_handler = 0; @@ -1925,13 +1827,14 @@ fm_tree_view_dispose (GObject *object) view->details->hidden_files_changed_id = 0; } - if (view->details->action_manager_changed_id != 0) { + if (view->details->actions_changed_id != 0) { g_signal_handler_disconnect (view->details->action_manager, - view->details->action_manager_changed_id); - view->details->action_manager_changed_id = 0; + view->details->actions_changed_id); + view->details->actions_changed_id = 0; } g_clear_object (&view->details->action_manager); + g_clear_object (&view->details->ui_manager); g_signal_handlers_disconnect_by_func (nemo_tree_sidebar_preferences, G_CALLBACK(filtering_changed_callback), @@ -1972,25 +1875,25 @@ fm_tree_view_class_init (FMTreeViewClass *class) } static void -fm_tree_view_set_parent_window (FMTreeView *sidebar, - NemoWindow *window) +fm_tree_view_set_parent_window (FMTreeView *view, + NemoWindow *window) { char *location; NemoWindowSlot *slot; - - sidebar->details->window = window; + + view->details->window = window; slot = nemo_window_get_active_slot (window); g_signal_connect_object (window, "loading_uri", - G_CALLBACK (loading_uri_callback), sidebar, 0); + G_CALLBACK (loading_uri_callback), view, 0); location = nemo_window_slot_get_current_uri (slot); - loading_uri_callback (window, location, sidebar); + loading_uri_callback (window, location, view); g_free (location); - sidebar->details->hidden_files_changed_id = + view->details->hidden_files_changed_id = g_signal_connect_object (window, "hidden-files-mode-changed", - G_CALLBACK (hidden_files_mode_changed_callback), sidebar, 0); + G_CALLBACK (hidden_files_mode_changed_callback), view, 0); } diff --git a/src/nemo-view.c b/src/nemo-view.c index 7fda4ad41..2ea55b61f 100644 --- a/src/nemo-view.c +++ b/src/nemo-view.c @@ -6334,119 +6334,90 @@ run_action_callback (NemoAction *action, gpointer callback_data) nemo_file_list_free (selected_files); } -typedef struct { - NemoView *view; - GList *selection; -} ActionVisibilityData; - static void -determine_visibility (gpointer data, gpointer callback_data) +update_actions_visibility (NemoView *view, GList *selection) { - NemoAction *action = NEMO_ACTION (data); - ActionVisibilityData *av_data = (ActionVisibilityData *) callback_data; - GList *selection; - NemoView *view; - - view = av_data->view; - selection = av_data->selection; - NemoFile *parent = nemo_view_get_directory_as_file (view); - if (nemo_action_get_visibility (action, selection, parent, FALSE, GTK_WINDOW (view->details->window))) { - gchar *label, *tt; - - label = nemo_action_get_label (action, selection, parent, GTK_WINDOW (view->details->window)); - tt = nemo_action_get_tt (action, selection, parent, GTK_WINDOW (view->details->window)); - - gtk_action_set_label (GTK_ACTION (action), label); - gtk_action_set_tooltip (GTK_ACTION (action), tt); - - g_free (label); - g_free (tt); - - gtk_action_set_visible (GTK_ACTION (action), TRUE); - } else { - gtk_action_set_visible (GTK_ACTION (action), FALSE); - } + nemo_action_manager_update_action_states (view->details->action_manager, + view->details->actions_action_group, + selection, + parent, + FALSE, + GTK_WINDOW (view->details->window)); } static void -update_actions_visibility (NemoView *view, GList *selection) +add_action_to_ui (NemoActionManager *manager, + GtkAction *action, + GtkUIManagerItemType type, + const gchar *path, + gpointer user_data) { - GList *actions = gtk_action_group_list_actions (view->details->actions_action_group); - ActionVisibilityData data; - - data.view = view; - data.selection = selection; - - g_list_foreach (actions, determine_visibility, &data); + NemoView *view = NEMO_VIEW (user_data); - g_list_free (actions); -} + if (type != GTK_UI_MANAGER_SEPARATOR) { + if (type == GTK_UI_MANAGER_MENUITEM) { + g_signal_handlers_disconnect_by_func (action, + run_action_callback, + view); -static void -add_action_to_action_menus (NemoView *directory_view, - NemoAction *action, - const char *menu_path, - const char *popup_path, - const char *popup_bg_path) -{ - GtkUIManager *ui_manager; + g_signal_connect (action, "activate", + G_CALLBACK (run_action_callback), + view); + } - const gchar *action_name = gtk_action_get_name (GTK_ACTION (action)); + gtk_action_group_add_action (view->details->actions_action_group, + action); + gtk_action_set_visible (GTK_ACTION (action), FALSE); + } - gtk_action_group_add_action (directory_view->details->actions_action_group, - GTK_ACTION (action)); + static const gchar *roots[] = { + NEMO_VIEW_MENU_PATH_ACTIONS_PLACEHOLDER, + NEMO_VIEW_POPUP_PATH_ACTIONS_PLACEHOLDER, + NEMO_VIEW_POPUP_PATH_BACKGROUND_ACTIONS_PLACEHOLDER, + NULL + }; - gtk_action_set_visible (GTK_ACTION (action), FALSE); + gint i = 0; + while (roots[i] != NULL) { + g_autofree gchar *full_path = NULL; + const gchar *name; - g_signal_handlers_disconnect_by_func (action, - run_action_callback, - directory_view); + if (path != NULL) { + full_path = g_strdup_printf ("%s/%s", roots[i], path); + } + else { + full_path = g_strdup (roots[i]); + } - g_signal_connect (action, "activate", - G_CALLBACK (run_action_callback), - directory_view); - ui_manager = nemo_window_get_ui_manager (directory_view->details->window); + if (type == GTK_UI_MANAGER_SEPARATOR) { + name = NULL; + } + else { + name = gtk_action_get_name (action); + } - gtk_ui_manager_add_ui (ui_manager, - directory_view->details->actions_merge_id, - menu_path, - action_name, - action_name, - GTK_UI_MANAGER_MENUITEM, - FALSE); - gtk_ui_manager_add_ui (ui_manager, - directory_view->details->actions_merge_id, - popup_path, - action_name, - action_name, - GTK_UI_MANAGER_MENUITEM, - FALSE); - gtk_ui_manager_add_ui (ui_manager, - directory_view->details->actions_merge_id, - popup_bg_path, - action_name, - action_name, - GTK_UI_MANAGER_MENUITEM, - FALSE); + gtk_ui_manager_add_ui (nemo_window_get_ui_manager (view->details->window), + view->details->actions_merge_id, + full_path, + name, + name, + type, + FALSE); + i++; + } } static void update_actions (NemoView *view) { - NemoAction *action; - GList *action_list, *node; + nemo_debug (NEMO_DEBUG_ACTIONS, "Refreshing menu actions"); - action_list = nemo_action_manager_list_actions (view->details->action_manager); - - for (node = action_list; node != NULL; node = node->next) { - action = node->data; - add_action_to_action_menus (view, action, NEMO_VIEW_MENU_PATH_ACTIONS_PLACEHOLDER, - NEMO_VIEW_POPUP_PATH_ACTIONS_PLACEHOLDER, - NEMO_VIEW_POPUP_PATH_BACKGROUND_ACTIONS_PLACEHOLDER); - } + nemo_action_manager_iterate_actions (view->details->action_manager, + (NemoActionManagerIterFunc) add_action_to_ui, + view); } static void @@ -9722,7 +9693,7 @@ real_update_menus (NemoView *view) nemo_view_can_rename_file (view, selection->data)); } - gtk_action_set_visible (action, !selection_contains_recent && + gtk_action_set_visible (action, !selection_contains_recent && !selection_contains_special_link); gboolean no_selection_or_one_dir = ((selection_count == 1 && selection_contains_directory) || From 3463a9b44af04e7ac16b106a53790a4caeef179b Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Tue, 30 Jan 2024 18:48:32 -0500 Subject: [PATCH 2/2] nemo-action.c: Add Locations and Files fields. These allow filename and glob matching against the selection's files or their parent: - Glob patterns (*.foo) can be used against filenames (not paths). - Exact filenames as well as full paths can be used for exact matches. See sample action for more behavior info. --- .../usr/share/nemo/actions/sample.nemo_action | 53 ++++- libnemo-private/nemo-action.c | 211 ++++++++++++++++++ 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/files/usr/share/nemo/actions/sample.nemo_action b/files/usr/share/nemo/actions/sample.nemo_action index 903dd21a4..6d8042383 100644 --- a/files/usr/share/nemo/actions/sample.nemo_action +++ b/files/usr/share/nemo/actions/sample.nemo_action @@ -122,4 +122,55 @@ Extensions=any; # sftp://joe@10.0.0.200/ matches # file:///home/joe/.bashrc does not -#UriScheme=file \ No newline at end of file +#UriScheme=file + +# Locations - semicolor-separated array of directory names and globs to check against the +# selection's parent location. +# +# If Locations is defined, an action will *only* be valid under one of the following conditions: +# - The current location's filename (foo.bar, no path) is captured by *at least one* +# of any defined glob patterns. +# - The current location's filename exactly matches *at least one* defined filename. +# +# If an entry is prefixed with '!', the above conditions are reversed, and an action is +# considered *invalid* if any matches are made. +# +# NOTE: Allowed patterns are considered before forbidden ones. This means that a location +# would be invalidated by not matching any allowed patterns before being invalidated +# by matching forbidden ones. +# +# Example desired condition: Match any dot-file locations except '.config': +# +# Locations=.*;!.config +# +# Action would be visible when right-clicking the file '.foo/bar', but not '.config/bar' +# +# Optional + +#Locations=.*;!.config; + +# Files - semicolor-separated array of file names and globs to check against the currently +# selected file list. +# +# If Files is defined, an action will *only* be valid under the following conditions: +# - All selected filenames (foo.bar, no path) are captured by *at least one* of any +# defined glob patterns. +# - All file absolute paths exactly match *at least one* of any defined absolute paths. +# - All filenames exactly match *at least one* of the defined filenames. +# +# If an entry is prefixed with '!', the above conditions are reversed, and an action is +# considered *invalid* if any matches are made. +# +# NOTE: Allowed patterns are considered before forbidden ones. This means that a filename +# would be invalidated by not matching any allowed patterns before being invalidated +# by matching forbidden ones. +# +# Example desired condition: Match .bash* but not '.bashrc': +# +# Files=.bash*;!.bashrc; +# +# Action would be visible when right-clicking the file '.bash_history', but not '.bashrc' +# +# Optional + +#Files=.bash*;!.bashrc; diff --git a/libnemo-private/nemo-action.c b/libnemo-private/nemo-action.c index 205fdcc94..331424506 100644 --- a/libnemo-private/nemo-action.c +++ b/libnemo-private/nemo-action.c @@ -35,6 +35,10 @@ #define g_drive_is_removable g_drive_is_media_removable #endif +#ifndef GLIB_VERSION_2_70 +#define g_pattern_spec_match g_pattern_match +#endif + typedef struct { SelectionType selection_type; gchar **extensions; @@ -58,6 +62,15 @@ typedef struct { gboolean run_in_terminal; gchar *uri_scheme; + GList *allowed_location_patterns; + GList *forbidden_location_patterns; + GList *allowed_location_filenames; + GList *forbidden_location_filenames; + + GList *allowed_patterns; + GList *forbidden_patterns; + GList *allowed_filenames; + GList *forbidden_filenames; gboolean constructing; } NemoActionPrivate; @@ -547,6 +560,51 @@ strip_custom_modifier (const gchar *raw, gboolean *custom, gchar **out) } } +static void +populate_patterns_and_filenames (NemoAction *action, + gchar **array, + GList **allowed_patterns, + GList **forbidden_patterns, + GList **allowed_filenames, + GList **forbidden_filenames) +{ + *allowed_patterns = NULL; + *forbidden_patterns = NULL; + *allowed_filenames = NULL; + *forbidden_filenames = NULL; + + if (array == NULL) { + return; + } + + gint i; + + for (i = 0; i < g_strv_length (array); i++) { + const gchar *str = array[i]; + + if (g_strstr_len (str, -1, "?") || g_strstr_len (str, -1, "*")) { + if (g_str_has_prefix (array[i], "!")) { + *forbidden_patterns = g_list_prepend (*forbidden_patterns, g_pattern_spec_new (array[i] + 1)); + } else { + *allowed_patterns = g_list_prepend (*allowed_patterns, g_pattern_spec_new (array[i])); + } + + continue; + } + + if (g_str_has_prefix (array[i], "!")) { + *forbidden_filenames = g_list_prepend (*forbidden_filenames, g_strdup (array[i] + 1)); + } else { + *allowed_filenames = g_list_prepend (*allowed_filenames, g_strdup (array[i])); + } + } + + *allowed_patterns = g_list_reverse (*allowed_patterns); + *forbidden_patterns = g_list_reverse (*forbidden_patterns); + *allowed_filenames = g_list_reverse (*allowed_filenames); + *forbidden_filenames = g_list_reverse (*forbidden_filenames); +} + void nemo_action_constructed (GObject *object) { @@ -679,6 +737,32 @@ nemo_action_constructed (GObject *object) KEY_TERMINAL, NULL); + gchar **locations = g_key_file_get_string_list (key_file, + ACTION_FILE_GROUP, + KEY_LOCATIONS, + NULL, + NULL); + + populate_patterns_and_filenames (action, locations, + &priv->allowed_location_patterns, + &priv->forbidden_location_patterns, + &priv->allowed_location_filenames, + &priv->forbidden_location_filenames); + g_strfreev (locations); + + gchar **files = g_key_file_get_string_list (key_file, + ACTION_FILE_GROUP, + KEY_FILES, + NULL, + NULL); + + populate_patterns_and_filenames (action, files, + &priv->allowed_patterns, + &priv->forbidden_patterns, + &priv->allowed_filenames, + &priv->forbidden_filenames); + g_strfreev (files); + gboolean is_desktop = FALSE; if (conditions && condition_count > 0) { @@ -907,6 +991,14 @@ nemo_action_finalize (GObject *object) g_list_free_full (priv->dbus, (GDestroyNotify) dbus_condition_free); g_list_free_full (priv->gsettings, (GDestroyNotify) gsettings_condition_free); + g_list_free_full (priv->allowed_location_patterns, (GDestroyNotify) g_pattern_spec_free); + g_list_free_full (priv->forbidden_location_patterns, (GDestroyNotify) g_pattern_spec_free); + g_list_free_full (priv->allowed_patterns, (GDestroyNotify) g_pattern_spec_free); + g_list_free_full (priv->forbidden_patterns, (GDestroyNotify) g_pattern_spec_free); + g_list_free_full (priv->allowed_location_filenames, (GDestroyNotify) g_free); + g_list_free_full (priv->forbidden_location_filenames, (GDestroyNotify) g_free); + g_list_free_full (priv->allowed_filenames, (GDestroyNotify) g_free); + g_list_free_full (priv->forbidden_filenames, (GDestroyNotify) g_free); g_clear_handle_id (&priv->dbus_recalc_timeout_id, g_source_remove); g_clear_handle_id (&priv->gsettings_recalc_timeout_id, g_source_remove); @@ -1548,7 +1640,113 @@ get_is_dir (NemoFile *file) return ret; } +static gboolean +check_is_allowed (NemoAction *action, + NemoFile *parent, + GList *selection, + GList *allowed_patterns, + GList *forbidden_patterns, + GList *allowed_names, + GList *forbidden_names) { + // If there are no pattern/name specs, always allow the location.. + if ((allowed_patterns == NULL && forbidden_patterns == NULL && allowed_names == NULL && forbidden_names == NULL)) { + return TRUE; + } + + GList *l, *files; + + if (parent != NULL) { + DEBUG ("Checking pattern/name matching for location: %s", nemo_file_peek_name (parent)); + files = g_list_prepend (NULL, parent); + } + else { + DEBUG ("Checking pattern/name matching for selected files"); + files = selection; + } + + gboolean allowed = TRUE; + + for (l = files; l != NULL; l = l->next) { + NemoFile *file = NEMO_FILE (l->data); + + g_autofree gchar *path = nemo_file_get_path (file); + // If the first file (or the single parent) isn't native, no need to + // check all selected items, just exit early. + if (path == NULL) { + goto check_allowed_done; + } + + const gchar *name = nemo_file_peek_name (file); + gboolean allowed_allowed = TRUE; + gboolean forbidden_allowed = TRUE; + GList *ll; + + if (allowed_patterns != NULL || allowed_names != NULL) { + allowed_allowed = FALSE; + + for (ll = allowed_patterns; ll != NULL; ll = ll->next) { + if (g_pattern_spec_match ((GPatternSpec *) ll->data, strlen (name), name, NULL)) { + allowed_allowed = TRUE; + break; + } + } + + for (ll = allowed_names; ll != NULL; ll = ll->next) { + gchar *aname = (gchar *) ll->data; + + if (name[0] == '/' && g_strcmp0 (path, aname) == 0) { + allowed_allowed = TRUE; + break; + } + else + if (g_str_has_suffix (name, aname)) { + allowed_allowed = TRUE; + break; + } + } + } + + if (forbidden_patterns != NULL || forbidden_names != NULL) { + // (forbidden_allowed = TRUE;) + + for (ll = forbidden_patterns; ll != NULL; ll = ll->next) { + if (g_pattern_spec_match ((GPatternSpec *) ll->data, strlen (name), name, NULL)) { + forbidden_allowed = FALSE; + break; + } + } + + for (ll = forbidden_names; ll != NULL; ll = ll->next) { + gchar *fname = (gchar *) ll->data; + if (name[0] == '/' && g_strcmp0 (path, fname) == 0) { + forbidden_allowed = FALSE; + break; + } + else + if (g_str_has_suffix (name, fname)) { + forbidden_allowed = FALSE; + break; + } + } + } + + DEBUG ("Final result - allowed pass: %d, forbidden pass: %d", allowed_allowed, forbidden_allowed); + if (!(allowed_allowed && forbidden_allowed)) { + allowed = FALSE; + break; + } + } + +check_allowed_done: + if (parent != NULL) { + g_list_free (files); + } + + return allowed; +} + + static gboolean get_visibility (NemoAction *action, GList *selection, @@ -1568,6 +1766,19 @@ get_visibility (NemoAction *action, return FALSE; } + if (!check_is_allowed (action, parent, NULL, + priv->allowed_location_patterns, + priv->forbidden_location_patterns, + priv->allowed_location_filenames, + priv->forbidden_location_filenames)) { + return FALSE; + } + + if (!check_is_allowed (action, NULL, selection, + priv->allowed_patterns, + priv->forbidden_patterns, + priv->allowed_filenames, + priv->forbidden_filenames)) { return FALSE; }