Files
cinnamon-tweaks/shortcut_manager.py
T

250 lines
9.0 KiB
Python
Executable File

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
cinnamon-shortcut-manager
==========================
A small GTK3 GUI for managing Cinnamon custom keyboard shortcuts: add, edit,
delete, and see everything currently registered - no manual editing of
Cinnamon Settings -> Keyboard -> Shortcuts -> Custom Shortcuts required.
Shortcuts are written straight to gsettings/dconf (see keybindings.py), the
same storage Cinnamon's own keybinding manager reads from, so anything
created here is live system-wide immediately, survives logout/login, and
shows up in Cinnamon Settings too.
Run directly: python3 shortcut_manager.py
"""
import os
import sys
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk, Gdk, GLib
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import keybindings
import presets
class AccelCaptureEntry(Gtk.Entry):
"""A read-only entry that captures the next key combo pressed into it."""
def __init__(self):
super().__init__()
self.set_editable(False)
self.set_placeholder_text("Click here, then press a key combo")
self.accelerator = ""
self.connect("key-press-event", self._onKeyPress)
self.connect("button-press-event", self._onFocusRequest)
def _onFocusRequest(self, widget, event):
self.grab_focus()
return False
def _onKeyPress(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self.setAccelerator("")
return True
if event.keyval == Gdk.KEY_BackSpace and not (event.state & Gtk.accelerator_get_default_mod_mask()):
self.setAccelerator("")
return True
accel = keybindings.keyEventToAccelerator(event.keyval, event.state)
if accel:
self.setAccelerator(accel)
return True
def setAccelerator(self, accel):
self.accelerator = accel
if not accel:
self.set_text("")
return
keyval, mods = Gtk.accelerator_parse(accel)
self.set_text(Gtk.accelerator_get_label(keyval, mods) if keyval else accel)
class ShortcutDialog(Gtk.Dialog):
def __init__(self, parent, shortcut=None):
title = "Edit shortcut" if shortcut else "Add shortcut"
super().__init__(title=title, transient_for=parent, modal=True)
self.set_default_size(420, -1)
self.add_buttons(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK,
)
self.editingId = shortcut.customId if shortcut else None
grid = Gtk.Grid(column_spacing=10, row_spacing=8, margin=12)
self.get_content_area().add(grid)
grid.attach(Gtk.Label(label="Name", xalign=0), 0, 0, 1, 1)
self.nameEntry = Gtk.Entry()
grid.attach(self.nameEntry, 1, 0, 1, 1)
grid.attach(Gtk.Label(label="Command", xalign=0), 0, 1, 1, 1)
self.commandEntry = Gtk.Entry()
grid.attach(self.commandEntry, 1, 1, 1, 1)
available = presets.availablePresets()
if available:
grid.attach(Gtk.Label(label="Preset", xalign=0), 0, 2, 1, 1)
presetCombo = Gtk.ComboBoxText()
presetCombo.append_text("(custom command)")
for label, _command in available:
presetCombo.append_text(label)
presetCombo.set_active(0)
presetCombo.connect("changed", self._onPresetChanged, available)
grid.attach(presetCombo, 1, 2, 1, 1)
grid.attach(Gtk.Label(label="Shortcut", xalign=0), 0, 3, 1, 1)
self.accelEntry = AccelCaptureEntry()
grid.attach(self.accelEntry, 1, 3, 1, 1)
self.warningLabel = Gtk.Label(xalign=0)
self.warningLabel.set_line_wrap(True)
self.warningLabel.get_style_context().add_class("warning")
grid.attach(self.warningLabel, 0, 4, 2, 1)
if shortcut:
self.nameEntry.set_text(shortcut.name)
self.commandEntry.set_text(shortcut.command)
self.accelEntry.setAccelerator(shortcut.binding)
self.accelEntry.connect("key-release-event", lambda w, e: self._refreshConflictWarning())
self.show_all()
self._refreshConflictWarning()
def _onPresetChanged(self, combo, available):
index = combo.get_active()
if index <= 0:
return
label, command = available[index - 1]
self.commandEntry.set_text(command)
if not self.nameEntry.get_text():
self.nameEntry.set_text(label)
def _refreshConflictWarning(self):
accel = self.accelEntry.accelerator
conflicts = keybindings.findConflicts(accel, excludeCustomId=self.editingId) if accel else []
if conflicts:
self.warningLabel.set_text("Conflicts with: " + ", ".join(conflicts))
else:
self.warningLabel.set_text("")
def result(self):
return self.nameEntry.get_text().strip(), self.commandEntry.get_text().strip(), self.accelEntry.accelerator
class ShortcutManagerWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Custom Shortcuts")
self.set_default_size(640, 420)
self.set_border_width(8)
rootBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
self.add(rootBox)
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
rootBox.pack_start(toolbar, False, False, 0)
addButton = Gtk.Button(label="Add")
addButton.connect("clicked", self._onAdd)
toolbar.pack_start(addButton, False, False, 0)
self.editButton = Gtk.Button(label="Edit")
self.editButton.connect("clicked", self._onEdit)
self.editButton.set_sensitive(False)
toolbar.pack_start(self.editButton, False, False, 0)
self.deleteButton = Gtk.Button(label="Delete")
self.deleteButton.connect("clicked", self._onDelete)
self.deleteButton.set_sensitive(False)
toolbar.pack_start(self.deleteButton, False, False, 0)
refreshButton = Gtk.Button(label="Refresh")
refreshButton.connect("clicked", lambda w: self._reload())
toolbar.pack_end(refreshButton, False, False, 0)
self.store = Gtk.ListStore(str, str, str, str)
self.treeView = Gtk.TreeView(model=self.store)
for index, columnTitle in enumerate(["Name", "Shortcut", "Command"]):
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(columnTitle, renderer, text=index)
column.set_resizable(True)
column.set_expand(index == 2)
self.treeView.append_column(column)
self.treeView.get_selection().connect("changed", self._onSelectionChanged)
self.treeView.connect("row-activated", lambda *_: self._onEdit(None))
scroller = Gtk.ScrolledWindow()
scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scroller.add(self.treeView)
rootBox.pack_start(scroller, True, True, 0)
self.connect("destroy", Gtk.main_quit)
self._reload()
def _reload(self):
self.store.clear()
for shortcut in keybindings.listShortcuts():
self.store.append([shortcut.name, shortcut.label(), shortcut.command, shortcut.customId])
def _onSelectionChanged(self, selection):
hasSelection = selection.get_selected()[1] is not None
self.editButton.set_sensitive(hasSelection)
self.deleteButton.set_sensitive(hasSelection)
def _selectedShortcut(self):
model, treeIter = self.treeView.get_selection().get_selected()
if not treeIter:
return None
customId = model[treeIter][3]
return next((s for s in keybindings.listShortcuts() if s.customId == customId), None)
def _onAdd(self, widget):
dialog = ShortcutDialog(self)
if dialog.run() == Gtk.ResponseType.OK:
name, command, binding = dialog.result()
if name and command and binding:
keybindings.addShortcut(name, command, binding)
self._reload()
dialog.destroy()
def _onEdit(self, widget):
shortcut = self._selectedShortcut()
if not shortcut:
return
dialog = ShortcutDialog(self, shortcut)
if dialog.run() == Gtk.ResponseType.OK:
name, command, binding = dialog.result()
if name and command:
keybindings.updateShortcut(shortcut.customId, name, command, binding)
self._reload()
dialog.destroy()
def _onDelete(self, widget):
shortcut = self._selectedShortcut()
if not shortcut:
return
confirm = Gtk.MessageDialog(
transient_for=self, modal=True, message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.YES_NO, text=f"Delete shortcut \"{shortcut.name}\"?",
)
response = confirm.run()
confirm.destroy()
if response == Gtk.ResponseType.YES:
keybindings.deleteShortcut(shortcut.customId)
self._reload()
def main():
window = ShortcutManagerWindow()
window.show_all()
Gtk.main()
if __name__ == "__main__":
main()