Skip to content

Commit cfdb8ca

Browse files
committed
layout editor: Add support for accelerator keys.
There is no conflict-detection for system shortcuts, but it will check nemo's built-in shortcuts as well as those defined for other actions.
1 parent a377912 commit cfdb8ca

File tree

6 files changed

+217
-11
lines changed

6 files changed

+217
-11
lines changed

action-layout-editor/meson.build

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,3 @@ install_data(
2929
install_dir: nemoDataPath / 'layout-editor',
3030
install_mode: 'rwxr-xr-x'
3131
)
32-
33-
install_data(
34-
'nemo-action-layout-editor.glade',
35-
install_dir: nemoDataPath / 'layout-editor'
36-
)

action-layout-editor/nemo_action_layout_editor.py

Lines changed: 202 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import gi
33
gi.require_version('Gtk', '3.0')
44
gi.require_version('XApp', '1.0')
5-
from gi.repository import Gtk, Gdk, GLib, Gio, XApp, GdkPixbuf, Pango
5+
gi.require_version('Xmlb', '2.0')
6+
from gi.repository import Gtk, Gdk, GLib, Gio, XApp, GdkPixbuf, Pango, Xmlb
67
import cairo
78
import json
89
from pathlib import Path
@@ -15,9 +16,11 @@
1516

1617
gettext.install(leconfig.PACKAGE, leconfig.LOCALE_DIR)
1718

19+
gresources = Gio.Resource.load(os.path.join(leconfig.PKG_DATADIR, "nemo-action-layout-editor-resources.gresource"))
20+
gresources._register()
21+
1822
JSON_FILE = Path(GLib.get_user_config_dir()).joinpath("nemo/actions-tree.json")
1923
USER_ACTIONS_DIR = Path(GLib.get_user_data_dir()).joinpath("nemo/actions")
20-
GLADE_FILE = Path(leconfig.PKG_DATADIR).joinpath("layout-editor/nemo-action-layout-editor.glade")
2124

2225
NON_SPICE_UUID_SUFFIX = "@untracked"
2326

@@ -30,6 +33,15 @@
3033
def new_hash():
3134
return uuid.uuid4().hex
3235

36+
class BuiltinShortcut():
37+
def __init__(self, label, accel_string):
38+
self.key, self.mods = Gtk.accelerator_parse(accel_string)
39+
40+
if self.key == 0 and self.mods == 0:
41+
self.label = "invalid (%s)" % accel_string
42+
43+
self.label = _(label)
44+
3345
class Row():
3446
def __init__(self, row_meta=None, keyfile=None, path=None, enabled=True):
3547
self.keyfile = keyfile
@@ -95,6 +107,17 @@ def get_label(self):
95107

96108
return label
97109

110+
def get_accelerator_string(self):
111+
if self.row_meta is not None:
112+
try:
113+
accel_string = self.row_meta['accelerator']
114+
if accel_string is not None:
115+
return accel_string
116+
except KeyError:
117+
pass
118+
119+
return None if accel_string == "" else None
120+
98121
def set_custom_label(self, label):
99122
if not self.row_meta:
100123
self.row_meta = {}
@@ -107,6 +130,12 @@ def set_custom_icon(self, icon):
107130

108131
self.row_meta['user-icon'] = icon
109132

133+
def set_accelerator_string(self, accel_string):
134+
if not self.row_meta:
135+
self.row_meta = {}
136+
137+
self.row_meta['accelerator'] = accel_string
138+
110139
def get_custom_label(self):
111140
if self.row_meta:
112141
return self.row_meta.get('user-label')
@@ -122,10 +151,13 @@ def __init__(self, window, builder=None):
122151
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
123152

124153
if builder is None:
125-
self.builder = Gtk.Builder.new_from_file(str(GLADE_FILE))
154+
self.builder = Gtk.Builder.new_from_resource("/org/nemo/action-layout-editor/nemo-action-layout-editor.glade")
126155
else:
127156
self.builder = builder
128157

158+
self.builtin_shortcuts = []
159+
self.load_nemo_shortcuts()
160+
129161
self.main_window = window
130162
self.layout_editor_box = self.builder.get_object("layout_editor_box")
131163
self.add(self.layout_editor_box)
@@ -202,8 +234,10 @@ def __init__(self, window, builder=None):
202234
visible=True
203235
)
204236

237+
# Icon and label
205238
column = Gtk.TreeViewColumn()
206239
self.treeview.append_column(column)
240+
column.set_expand(True)
207241

208242
cell = Gtk.CellRendererPixbuf()
209243
column.pack_start(cell, False)
@@ -212,6 +246,27 @@ def __init__(self, window, builder=None):
212246
column.pack_start(cell, False)
213247
column.set_cell_data_func(cell, self.menu_label_render_func)
214248

249+
# Accelerators
250+
column = Gtk.TreeViewColumn()
251+
column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
252+
column.set_expand(True)
253+
self.treeview.append_column(column)
254+
255+
cell = Gtk.CellRendererAccel()
256+
cell.set_property("editable", True)
257+
cell.set_property("xalign", 0)
258+
column.pack_end(cell, False)
259+
column.set_cell_data_func(cell, self.accel_render_func)
260+
261+
layout = self.treeview.create_pango_layout(_("Click to add a shortcut"))
262+
w, h = layout.get_pixel_size()
263+
column.set_min_width(w + 20)
264+
265+
cell.connect("editing-started", self.on_accel_edit_started)
266+
cell.connect("accel-edited", self.on_accel_edited)
267+
cell.connect("accel-cleared", self.on_accel_cleared)
268+
self.editing_accel = False
269+
215270
self.treeview_holder.add(self.treeview)
216271

217272
self.save_button.connect("clicked", self.on_save_clicked)
@@ -257,6 +312,27 @@ def __init__(self, window, builder=None):
257312
self.update_arrow_button_states()
258313
self.set_needs_saved(False)
259314

315+
def load_nemo_shortcuts(self):
316+
source = Xmlb.BuilderSource()
317+
try:
318+
xml = Gio.resources_lookup_data("/org/nemo/action-layout-editor/nemo-shortcuts.ui", Gio.ResourceLookupFlags.NONE)
319+
ret = source.load_bytes(xml, Xmlb.BuilderSourceFlags.NONE)
320+
builder = Xmlb.Builder()
321+
builder.import_source(source)
322+
silo = builder.compile(Xmlb.BuilderCompileFlags.NONE, None)
323+
except GLib.Error as e:
324+
print("Could not load nemo-shortcuts.ui from resource file - we won't be able to detect built-in shortcut collisions: %s" % e.message)
325+
return
326+
327+
root = silo.query_first("interface")
328+
for child in root.query(f"object/child", 0):
329+
for section in child.query("object[@class='GtkShortcutsSection']", 0):
330+
for group in section.query("child/object[@class='GtkShortcutsGroup']", 0):
331+
for shortcut in group.query("child/object[@class='GtkShortcutsShortcut']", 0):
332+
label = shortcut.query_text("property[@name='title']")
333+
accel = shortcut.query_text("property[@name='accelerator']")
334+
self.builtin_shortcuts.append(BuiltinShortcut(label, accel))
335+
260336
def reload_model(self, flat=False):
261337
self.updating_model = True
262338
self.model.clear()
@@ -367,6 +443,17 @@ def validate_node(self, node):
367443
# not mandatory
368444
pass
369445

446+
# Check the node has a valid accelerator
447+
try:
448+
accel_str = node['accelerator']
449+
450+
if accel_str not in ("", None):
451+
key, mods = Gtk.accelerator_parse(accel_str)
452+
if key == 0 and mods == 0:
453+
raise ValueError("%s: Invalid accelerator string '%s'" % (uuid, accel_str))
454+
except KeyError:
455+
pass
456+
370457
# Check that the node has a valid children list
371458
try:
372459
children = node['children']
@@ -503,7 +590,8 @@ def serialize_model(self, parent, model):
503590
'uuid': uuid,
504591
'type': row_type,
505592
'user-label': row.get_custom_label(),
506-
'user-icon': row.get_custom_icon()
593+
'user-icon': row.get_custom_icon(),
594+
'accelerator': row.get_accelerator_string()
507595
}
508596

509597
if row_type == ROW_TYPE_SUBMENU:
@@ -800,6 +888,90 @@ def on_action_enabled_switch_notify(self, switch, pspec):
800888
self.selected_row_changed(needs_saved=False)
801889
self.save_disabled_list()
802890

891+
def on_accel_edited(self, accel, path, key, mods, kc, data=None):
892+
if not self.validate_accelerator(key, mods):
893+
return
894+
895+
row = self.get_selected_row_field(ROW_OBJ)
896+
if row is not None:
897+
row.set_accelerator_string(Gtk.accelerator_name(key, mods))
898+
self.selected_row_changed()
899+
900+
def on_accel_cleared(self, accel, path, data=None):
901+
row = self.get_selected_row_field(ROW_OBJ)
902+
if row is not None:
903+
row.set_accelerator_string(None)
904+
self.selected_row_changed()
905+
906+
def on_accel_edit_started(self, cell, editable, path, data=None):
907+
self.editing_accel = True
908+
editable.connect("editing-done", self.accel_editing_done)
909+
910+
def accel_editing_done(self, editable, data=None):
911+
self.editing_accel = False
912+
editable.disconnect_by_func(self.accel_editing_done)
913+
914+
def validate_accelerator(self, key, mods):
915+
# Check nemo's built-ins (copy, paste, etc...)
916+
for shortcut in self.builtin_shortcuts:
917+
if shortcut.key == key and shortcut.mods == mods:
918+
dialog = Gtk.MessageDialog(
919+
transient_for=self.main_window,
920+
modal=True,
921+
message_type=Gtk.MessageType.ERROR,
922+
buttons=Gtk.ButtonsType.OK,
923+
text=_("This key combination is already in use by Nemo (<b>%s</b>). It cannot be changed.") % shortcut.label,
924+
use_markup=True
925+
)
926+
dialog.run()
927+
dialog.destroy()
928+
return False
929+
930+
conflict = False
931+
932+
def check_for_action_conflict(iter):
933+
foreach_iter = self.model.iter_children(iter)
934+
935+
nonlocal conflict
936+
937+
while not conflict and foreach_iter is not None:
938+
row = self.model.get_value(foreach_iter, ROW_OBJ)
939+
accel_string = row.get_accelerator_string()
940+
941+
if accel_string is not None:
942+
row_key, row_mod = Gtk.accelerator_parse(accel_string)
943+
if row_key == key and row_mod == mods:
944+
dialog = Gtk.MessageDialog(
945+
transient_for=self.main_window,
946+
modal=True,
947+
message_type=Gtk.MessageType.WARNING,
948+
buttons=Gtk.ButtonsType.YES_NO,
949+
text=_("This key combination is already in use by another action:"
950+
"\n\n<b>%s</b>\n\n"
951+
"Do you want to replace it?") % row.get_label(),
952+
use_markup=True
953+
)
954+
resp = dialog.run()
955+
dialog.destroy()
956+
957+
# nonlocal conflict
958+
959+
if resp == Gtk.ResponseType.YES:
960+
row.set_accelerator_string(None)
961+
conflict = False
962+
else:
963+
conflict = True
964+
break
965+
966+
foreach_type = self.model.get_value(foreach_iter, ROW_TYPE)
967+
if foreach_type == ROW_TYPE_SUBMENU:
968+
check_for_action_conflict(foreach_iter)
969+
970+
foreach_iter = self.model.iter_next(foreach_iter)
971+
972+
check_for_action_conflict(None)
973+
return not conflict
974+
803975
# Cell render functions
804976

805977
def menu_icon_render_func(self, column, cell, model, iter, data):
@@ -823,6 +995,31 @@ def menu_label_render_func(self, column, cell, model, iter, data):
823995
cell.set_property("markup", row.get_label())
824996
cell.set_property("weight", Pango.Weight.NORMAL if row.enabled else Pango.Weight.ULTRALIGHT)
825997

998+
def accel_render_func(self, column, cell, model, iter, data):
999+
row_type = model.get_value(iter, ROW_TYPE)
1000+
if row_type in (ROW_TYPE_SUBMENU, ROW_TYPE_SEPARATOR):
1001+
cell.set_property("visible", False)
1002+
return
1003+
1004+
row = model.get_value(iter, ROW_OBJ)
1005+
1006+
accel_string = row.get_accelerator_string() or ""
1007+
key, mods = Gtk.accelerator_parse(accel_string)
1008+
cell.set_property("visible", True)
1009+
cell.set_property("accel-key", key)
1010+
cell.set_property("accel-mods", mods)
1011+
1012+
if accel_string == "":
1013+
spath, siter = self.get_selected_row_path_iter()
1014+
current_path = model.get_path(iter)
1015+
if current_path is not None and current_path.compare(spath) == 0:
1016+
if not self.editing_accel:
1017+
cell.set_property("text", _("Click to add a shortcut"))
1018+
else:
1019+
cell.set_property("text", None)
1020+
else:
1021+
cell.set_property("text", " ")
1022+
8261023
# DND
8271024

8281025
def on_drag_begin(self, widget, context):
@@ -1302,7 +1499,7 @@ def quit(self, *args, **kwargs):
13021499

13031500
class EditorWindow():
13041501
def __init__(self):
1305-
self.builder = Gtk.Builder.new_from_file(str(GLADE_FILE))
1502+
self.builder = Gtk.Builder.new_from_resource("/org/nemo/action-layout-editor/nemo-action-layout-editor.glade")
13061503
self.main_window = self.builder.get_object("main_window")
13071504
self.hamburger_button = self.builder.get_object("hamburger_button")
13081505
self.editor = NemoActionsOrganizer(self.main_window, self.builder)

debian/control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ Description: file manager and graphical shell for Cinnamon
119119

120120
Package: nemo-data
121121
Architecture: all
122-
Depends: python3, ${misc:Depends}, ${python3:Depends}
122+
Depends: python3, gir1.2-xmlb-2.0, ${misc:Depends}, ${python3:Depends}
123123
Suggests: nemo
124124
Description: data files for nemo
125125
Nemo is the official file manager and graphical shell for the

gresources/meson.build

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ gresources = gnome.compile_resources(
66
install: false
77
)
88

9+
layout_editor_gresources = gnome.compile_resources(
10+
'nemo-action-layout-editor-resources', 'nemo-action-layout-editor.gresource.xml',
11+
source_dir: '.',
12+
gresource_bundle: true,
13+
install: true,
14+
install_dir: nemoDataPath
15+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<gresources>
3+
<gresource prefix="/org/nemo/action-layout-editor/">
4+
<file>nemo-action-layout-editor.glade</file>
5+
<file>nemo-shortcuts.ui</file>
6+
</gresource>
7+
</gresources>

0 commit comments

Comments
 (0)