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