1#!/usr/local/bin/python3.8
2
3from gi.repository import Gtk, Gdk, GObject
4
5FORBIDDEN_KEYVALS = [
6    Gdk.KEY_Home,
7    Gdk.KEY_Left,
8    Gdk.KEY_Up,
9    Gdk.KEY_Right,
10    Gdk.KEY_Down,
11    Gdk.KEY_Page_Up,
12    Gdk.KEY_Page_Down,
13    Gdk.KEY_End,
14    Gdk.KEY_Tab,
15    Gdk.KEY_Return,
16    Gdk.KEY_space,
17    Gdk.KEY_Mode_switch,
18    Gdk.KEY_KP_0, # numerics currently are recogized only as _End, _Down, etc.. with or without numlock
19    Gdk.KEY_KP_1, # Gdk checks numlock and parses out the correct key, but this could change, so list
20    Gdk.KEY_KP_2, # these numerics anyhow. (This may differ depending on kb layouts, locales, etc.. but
21    Gdk.KEY_KP_3, # I didn't thoroughly check.)
22    Gdk.KEY_KP_4,
23    Gdk.KEY_KP_5,
24    Gdk.KEY_KP_6,
25    Gdk.KEY_KP_7,
26    Gdk.KEY_KP_8,
27    Gdk.KEY_KP_9,
28    Gdk.KEY_KP_End,
29    Gdk.KEY_KP_Down,
30    Gdk.KEY_KP_Next,
31    Gdk.KEY_KP_Left,
32    Gdk.KEY_KP_Begin,
33    Gdk.KEY_KP_Right,
34    Gdk.KEY_KP_Home,
35    Gdk.KEY_KP_Up,
36    Gdk.KEY_KP_Prior,
37    Gdk.KEY_KP_Insert,
38    Gdk.KEY_KP_Delete,
39    Gdk.KEY_KP_Add,
40    Gdk.KEY_KP_Subtract,
41    Gdk.KEY_KP_Multiply,
42    Gdk.KEY_KP_Divide,
43    Gdk.KEY_KP_Enter,
44    Gdk.KEY_Num_Lock
45]
46
47class ButtonKeybinding(Gtk.TreeView):
48    __gsignals__ = {
49        'accel-edited': (GObject.SignalFlags.RUN_LAST, None, (str, str)),
50        'accel-cleared': (GObject.SignalFlags.RUN_LAST, None, ())
51    }
52
53    __gproperties__ = {
54        "accel-string": (str,
55                         "accelerator string",
56                         "Parseable accelerator string",
57                         None,
58                         GObject.ParamFlags.READWRITE)
59    }
60
61    def __init__(self):
62        super(ButtonKeybinding, self).__init__()
63
64        self.set_headers_visible(False)
65        self.set_enable_search(False)
66        self.set_hover_selection(True)
67        self.set_tooltip_text(CellRendererKeybinding.TOOLTIP_TEXT)
68
69        self.entry_store = None
70        self.accel_string = ""
71        self.keybinding_cell = CellRendererKeybinding(a_widget=self)
72        self.keybinding_cell.set_alignment(.5,.5)
73        self.keybinding_cell.connect('accel-edited', self.on_cell_edited)
74        self.keybinding_cell.connect('accel-cleared', self.on_cell_cleared)
75
76        col = Gtk.TreeViewColumn("binding", self.keybinding_cell, accel_string=0)
77        col.set_alignment(.5)
78
79        self.append_column(col)
80
81        self.keybinding_cell.set_property('editable', True)
82
83        self.load_model()
84
85        self.connect("focus-out-event", self.on_focus_lost)
86
87    def on_cell_edited(self, cell, path, accel_string, accel_label):
88        self.accel_string = accel_string
89        self.emit("accel-edited", accel_string, accel_label)
90        self.load_model()
91
92    def on_cell_cleared(self, cell, path):
93        self.accel_string = ""
94        self.emit("accel-cleared")
95        self.load_model()
96
97    def on_focus_lost(self, widget, event):
98        self.get_selection().unselect_all()
99
100    def load_model(self):
101        if self.entry_store:
102            self.entry_store.clear()
103
104        self.entry_store = Gtk.ListStore(str) # Accel string
105        self.entry_store.append((self.accel_string,))
106
107        self.set_model(self.entry_store)
108
109    def do_get_property(self, prop):
110        if prop.name == 'accel-string':
111            return self.accel_string
112        else:
113            raise AttributeError('unknown property %s' % prop.name)
114
115    def do_set_property(self, prop, value):
116        if prop.name == 'accel-string':
117            if value != self.accel_string:
118                self.accel_string = value
119                self.keybinding_cell.set_value(value)
120        else:
121            raise AttributeError('unknown property %s' % prop.name)
122
123    def get_accel_string(self):
124        return self.accel_string
125
126    def set_accel_string(self, accel_string):
127        self.accel_string = accel_string
128        self.load_model()
129
130
131class CellRendererKeybinding(Gtk.CellRendererText):
132    __gsignals__ = {
133        'accel-edited': (GObject.SignalFlags.RUN_LAST, None, (str, str, str)),
134        'accel-cleared': (GObject.SignalFlags.RUN_LAST, None, (str,))
135    }
136
137    __gproperties__ = {
138        "accel-string": (str,
139                         "accelerator string",
140                         "Parseable accelerator string",
141                         None,
142                         GObject.ParamFlags.READWRITE)
143    }
144
145    TOOLTIP_TEXT = "%s\n%s\n%s" % (_("Click to set a new accelerator key."),
146                                   _("Press Escape or click again to cancel the operation."),
147                                   _("Press Backspace to clear the existing keybinding."))
148
149    def __init__(self, a_widget, accel_string=None):
150        super(CellRendererKeybinding, self).__init__()
151        self.connect("editing-started", self.editing_started)
152        self.release_event_id = 0
153        self.press_event_id = 0
154        self.focus_id = 0
155
156        self.a_widget = a_widget
157        self.accel_string = accel_string
158
159        self.path = None
160        self.press_event = None
161        self.teaching = False
162
163        self.update_label()
164
165    def do_get_property(self, prop):
166        if prop.name == 'accel-string':
167            return self.accel_string
168        else:
169            raise AttributeError('unknown property %s' % prop.name)
170
171    def do_set_property(self, prop, value):
172        if prop.name == 'accel-string':
173            if value != self.accel_string:
174                self.accel_string = value
175                self.update_label()
176        else:
177            raise AttributeError('unknown property %s' % prop.name)
178
179    def update_label(self):
180        text = _("unassigned")
181        if self.accel_string:
182            key, codes, mods = Gtk.accelerator_parse_with_keycode(self.accel_string)
183            if codes is not None and len(codes) > 0:
184                text = Gtk.accelerator_get_label_with_keycode(None, key, codes[0], mods)
185        self.set_property("text", text)
186
187    def set_value(self, accel_string=None):
188        self.set_property("accel-string", accel_string)
189
190    def editing_started(self, renderer, editable, path):
191        if not self.teaching:
192            self.path = path
193            device = Gtk.get_current_event_device()
194            if device.get_source() == Gdk.InputSource.KEYBOARD:
195                self.keyboard = device
196            else:
197                self.keyboard = device.get_associated_device()
198
199            self.keyboard.grab(self.a_widget.get_window(), Gdk.GrabOwnership.WINDOW, False,
200                               Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK,
201                               None, Gdk.CURRENT_TIME)
202
203            editable.set_text(_("Pick an accelerator"))
204            self.accel_editable = editable
205
206            self.release_event_id = self.accel_editable.connect( "key-release-event", self.on_key_release )
207            self.press_event_id = self.accel_editable.connect( "key-press-event", self.on_key_press )
208            self.focus_id = self.accel_editable.connect( "focus-out-event", self.on_focus_out )
209            self.teaching = True
210        else:
211            self.ungrab()
212            self.update_label()
213            self.teaching = False
214
215    def on_focus_out(self, widget, event):
216        self.teaching = False
217        self.ungrab()
218
219    def on_key_press(self, widget, event):
220        if self.teaching:
221            self.press_event = event.copy()
222            return True
223
224        return False
225
226    def on_key_release(self, widget, event):
227        self.ungrab()
228        self.teaching = False
229        event = self.press_event
230
231        display = widget.get_display()
232
233        keyval = 0
234        group = event.group
235        accel_mods = event.state
236
237        # HACK: we don't want to use SysRq as a keybinding (but we do
238        # want Alt+Print), so we avoid translation from Alt+Print to SysRq
239
240        if event.keyval == Gdk.KEY_Sys_Req and \
241           ((accel_mods & Gdk.ModifierType.MOD1_MASK) != 0):
242            keyval = Gdk.KEY_Print
243            consumed_modifiers = 0
244        else:
245            keymap = Gdk.Keymap.get_for_display(display)
246            group_mask_disabled = False
247            shift_group_mask = 0
248
249            shift_group_mask = keymap.get_modifier_mask(Gdk.ModifierIntent.SHIFT_GROUP)
250
251            retval, keyval, effective_group, level, consumed_modifiers = \
252                keymap.translate_keyboard_state(event.hardware_keycode, accel_mods, group)
253
254            if group_mask_disabled:
255                effective_group = 1
256
257            if consumed_modifiers:
258                consumed_modifiers &= ~shift_group_mask
259
260        accel_key = Gdk.keyval_to_lower(keyval)
261        if accel_key == Gdk.KEY_ISO_Left_Tab:
262            accel_key = Gdk.KEY_Tab
263
264        accel_mods &= Gtk.accelerator_get_default_mod_mask()
265
266        if accel_mods == 0:
267            if accel_key == Gdk.KEY_Escape:
268                self.update_label()
269                self.teaching = False
270                self.path = None
271                self.press_event = None
272                return True
273            elif accel_key == Gdk.KEY_BackSpace:
274                self.teaching = False
275                self.press_event = None
276                self.set_value(None)
277                self.emit("accel-cleared", self.path)
278                self.path = None
279                return True
280
281        accel_string = Gtk.accelerator_name_with_keycode(None, accel_key, event.hardware_keycode, Gdk.ModifierType(accel_mods))
282        accel_label = Gtk.accelerator_get_label_with_keycode(None, accel_key, event.hardware_keycode, Gdk.ModifierType(accel_mods))
283
284        # print("accel_mods: %d, keyval: %d, Storing %s as %s" % (accel_mods, keyval, accel_label, accel_string))
285
286        if (accel_mods == 0 or accel_mods == Gdk.ModifierType.SHIFT_MASK) and event.hardware_keycode != 0:
287            if ((keyval >= Gdk.KEY_a                    and keyval <= Gdk.KEY_z)
288                or  (keyval >= Gdk.KEY_A                    and keyval <= Gdk.KEY_Z)
289                or  (keyval >= Gdk.KEY_0                    and keyval <= Gdk.KEY_9)
290                or  (keyval >= Gdk.KEY_kana_fullstop        and keyval <= Gdk.KEY_semivoicedsound)
291                or  (keyval >= Gdk.KEY_Arabic_comma         and keyval <= Gdk.KEY_Arabic_sukun)
292                or  (keyval >= Gdk.KEY_Serbian_dje          and keyval <= Gdk.KEY_Cyrillic_HARDSIGN)
293                or  (keyval >= Gdk.KEY_Greek_ALPHAaccent    and keyval <= Gdk.KEY_Greek_omega)
294                or  (keyval >= Gdk.KEY_hebrew_doublelowline and keyval <= Gdk.KEY_hebrew_taf)
295                or  (keyval >= Gdk.KEY_Thai_kokai           and keyval <= Gdk.KEY_Thai_lekkao)
296                or  (keyval >= Gdk.KEY_Hangul               and keyval <= Gdk.KEY_Hangul_Special)
297                or  (keyval >= Gdk.KEY_Hangul_Kiyeog        and keyval <= Gdk.KEY_Hangul_J_YeorinHieuh)
298                    or  keyval in FORBIDDEN_KEYVALS):
299                dialog = Gtk.MessageDialog(None,
300                                           Gtk.DialogFlags.DESTROY_WITH_PARENT,
301                                           Gtk.MessageType.ERROR,
302                                           Gtk.ButtonsType.OK,
303                                           None)
304                dialog.set_default_size(400, 200)
305                msg = _("\nThis key combination, \'<b>%s</b>\' cannot be used because it would become impossible to type using this key.\n\n")
306                msg += _("Please try again with a modifier key such as Control, Alt or Super (Windows key) at the same time.\n")
307                dialog.set_markup(msg % (accel_label))
308                dialog.show_all()
309                response = dialog.run()
310                dialog.destroy()
311                return True
312
313        self.press_event = None
314        self.set_value(accel_string)
315        self.emit("accel-edited", self.path, accel_string, accel_label)
316        self.path = None
317
318        return True
319
320    def ungrab(self):
321        self.keyboard.ungrab(Gdk.CURRENT_TIME)
322        if self.release_event_id > 0:
323            self.accel_editable.disconnect(self.release_event_id)
324            self.release_event_id = 0
325        if self.press_event_id > 0:
326            self.accel_editable.disconnect(self.press_event_id)
327            self.press_event_id = 0
328        if self.focus_id > 0:
329            self.accel_editable.disconnect(self.focus_id)
330            self.focus_id = 0
331        try:
332            self.accel_editable.editing_done()
333            self.accel_editable.remove_widget()
334        except:
335            pass
336