2
2
import gi
3
3
gi .require_version ('Gtk' , '3.0' )
4
4
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
6
7
import cairo
7
8
import json
8
9
from pathlib import Path
15
16
16
17
gettext .install (leconfig .PACKAGE , leconfig .LOCALE_DIR )
17
18
19
+ gresources = Gio .Resource .load (os .path .join (leconfig .PKG_DATADIR , "nemo-action-layout-editor-resources.gresource" ))
20
+ gresources ._register ()
21
+
18
22
JSON_FILE = Path (GLib .get_user_config_dir ()).joinpath ("nemo/actions-tree.json" )
19
23
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" )
21
24
22
25
NON_SPICE_UUID_SUFFIX = "@untracked"
23
26
30
33
def new_hash ():
31
34
return uuid .uuid4 ().hex
32
35
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
+
33
45
class Row ():
34
46
def __init__ (self , row_meta = None , keyfile = None , path = None , enabled = True ):
35
47
self .keyfile = keyfile
@@ -95,6 +107,17 @@ def get_label(self):
95
107
96
108
return label
97
109
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
+
98
121
def set_custom_label (self , label ):
99
122
if not self .row_meta :
100
123
self .row_meta = {}
@@ -107,6 +130,12 @@ def set_custom_icon(self, icon):
107
130
108
131
self .row_meta ['user-icon' ] = icon
109
132
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
+
110
139
def get_custom_label (self ):
111
140
if self .row_meta :
112
141
return self .row_meta .get ('user-label' )
@@ -122,10 +151,13 @@ def __init__(self, window, builder=None):
122
151
Gtk .Box .__init__ (self , orientation = Gtk .Orientation .VERTICAL )
123
152
124
153
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" )
126
155
else :
127
156
self .builder = builder
128
157
158
+ self .builtin_shortcuts = []
159
+ self .load_nemo_shortcuts ()
160
+
129
161
self .main_window = window
130
162
self .layout_editor_box = self .builder .get_object ("layout_editor_box" )
131
163
self .add (self .layout_editor_box )
@@ -202,8 +234,10 @@ def __init__(self, window, builder=None):
202
234
visible = True
203
235
)
204
236
237
+ # Icon and label
205
238
column = Gtk .TreeViewColumn ()
206
239
self .treeview .append_column (column )
240
+ column .set_expand (True )
207
241
208
242
cell = Gtk .CellRendererPixbuf ()
209
243
column .pack_start (cell , False )
@@ -212,6 +246,27 @@ def __init__(self, window, builder=None):
212
246
column .pack_start (cell , False )
213
247
column .set_cell_data_func (cell , self .menu_label_render_func )
214
248
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
+
215
270
self .treeview_holder .add (self .treeview )
216
271
217
272
self .save_button .connect ("clicked" , self .on_save_clicked )
@@ -257,6 +312,27 @@ def __init__(self, window, builder=None):
257
312
self .update_arrow_button_states ()
258
313
self .set_needs_saved (False )
259
314
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
+
260
336
def reload_model (self , flat = False ):
261
337
self .updating_model = True
262
338
self .model .clear ()
@@ -367,6 +443,17 @@ def validate_node(self, node):
367
443
# not mandatory
368
444
pass
369
445
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
+
370
457
# Check that the node has a valid children list
371
458
try :
372
459
children = node ['children' ]
@@ -503,7 +590,8 @@ def serialize_model(self, parent, model):
503
590
'uuid' : uuid ,
504
591
'type' : row_type ,
505
592
'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 ()
507
595
}
508
596
509
597
if row_type == ROW_TYPE_SUBMENU :
@@ -800,6 +888,90 @@ def on_action_enabled_switch_notify(self, switch, pspec):
800
888
self .selected_row_changed (needs_saved = False )
801
889
self .save_disabled_list ()
802
890
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
+
803
975
# Cell render functions
804
976
805
977
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):
823
995
cell .set_property ("markup" , row .get_label ())
824
996
cell .set_property ("weight" , Pango .Weight .NORMAL if row .enabled else Pango .Weight .ULTRALIGHT )
825
997
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
+
826
1023
# DND
827
1024
828
1025
def on_drag_begin (self , widget , context ):
@@ -1302,7 +1499,7 @@ def quit(self, *args, **kwargs):
1302
1499
1303
1500
class EditorWindow ():
1304
1501
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" )
1306
1503
self .main_window = self .builder .get_object ("main_window" )
1307
1504
self .hamburger_button = self .builder .get_object ("hamburger_button" )
1308
1505
self .editor = NemoActionsOrganizer (self .main_window , self .builder )
0 commit comments