feat: add shortcut-manager for GNOME keybinding presets
This commit is contained in:
commit
32dec59d45
4 files changed
+467
No files matched your search
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Installs cinnamon-shortcut-manager for the current user:
|
||||||
|
# - copies the app into ~/.local/share/cinnamon-shortcut-manager/
|
||||||
|
# - creates a menu launcher (~/.local/share/applications/)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
APP_DIR="$HOME/.local/share/cinnamon-shortcut-manager"
|
||||||
|
DESKTOP_FILE="$HOME/.local/share/applications/cinnamon-shortcut-manager.desktop"
|
||||||
|
|
||||||
|
mkdir -p "$APP_DIR" "$(dirname "$DESKTOP_FILE")"
|
||||||
|
cp "$SRC_DIR/shortcut_manager.py" "$SRC_DIR/keybindings.py" "$SRC_DIR/presets.py" "$APP_DIR/"
|
||||||
|
chmod +x "$APP_DIR/shortcut_manager.py"
|
||||||
|
|
||||||
|
cat > "$DESKTOP_FILE" << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Custom Shortcuts
|
||||||
|
Comment=Add, edit and delete custom Cinnamon keyboard shortcuts
|
||||||
|
Exec=python3 $APP_DIR/shortcut_manager.py
|
||||||
|
Icon=preferences-desktop-keyboard-shortcuts
|
||||||
|
Terminal=false
|
||||||
|
Categories=Settings;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Installed -> $APP_DIR"
|
||||||
|
echo "Menu launcher -> $DESKTOP_FILE"
|
||||||
|
echo "Run directly with: python3 $APP_DIR/shortcut_manager.py"
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
"""
|
||||||
|
Backend for reading/writing Cinnamon custom keyboard shortcuts.
|
||||||
|
|
||||||
|
Cinnamon (like GNOME) stores custom shortcuts as a relocatable schema:
|
||||||
|
each shortcut lives at /org/cinnamon/desktop/keybindings/custom-keybindings/customN/
|
||||||
|
under the "org.cinnamon.desktop.keybindings.custom-keybinding" schema, with
|
||||||
|
three keys: name, command, binding (an array of accelerator strings, in
|
||||||
|
practice always a single entry). The top-level
|
||||||
|
"org.cinnamon.desktop.keybindings" schema holds "custom-list", the array of
|
||||||
|
customN ids Cinnamon actually reads at login. Writing straight to gsettings
|
||||||
|
is enough to make the shortcut live system-wide - no daemon or restart
|
||||||
|
needed, Cinnamon's keybinding manager watches these keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gio, Gtk
|
||||||
|
|
||||||
|
LIST_SCHEMA = "org.cinnamon.desktop.keybindings"
|
||||||
|
ITEM_SCHEMA = "org.cinnamon.desktop.keybindings.custom-keybinding"
|
||||||
|
ITEM_PATH_ROOT = "/org/cinnamon/desktop/keybindings/custom-keybindings/"
|
||||||
|
|
||||||
|
CONFLICT_SCHEMAS = [
|
||||||
|
"org.cinnamon.desktop.keybindings",
|
||||||
|
"org.cinnamon.desktop.keybindings.wm",
|
||||||
|
"org.cinnamon.desktop.keybindings.media-keys",
|
||||||
|
"org.gnome.desktop.wm.keybindings",
|
||||||
|
"org.cinnamon.muffin.keybindings",
|
||||||
|
]
|
||||||
|
|
||||||
|
IGNORED_KEYVALS = {
|
||||||
|
"Shift_L", "Shift_R", "Control_L", "Control_R", "Alt_L", "Alt_R",
|
||||||
|
"Super_L", "Super_R", "Meta_L", "Meta_R", "Caps_Lock", "Num_Lock",
|
||||||
|
"ISO_Level3_Shift",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Shortcut:
|
||||||
|
def __init__(self, customId, name, command, binding):
|
||||||
|
self.customId = customId
|
||||||
|
self.name = name
|
||||||
|
self.command = command
|
||||||
|
self.binding = binding
|
||||||
|
|
||||||
|
def label(self):
|
||||||
|
if not self.binding:
|
||||||
|
return "(unset)"
|
||||||
|
keyval, mods = Gtk.accelerator_parse(self.binding)
|
||||||
|
if not keyval:
|
||||||
|
return self.binding
|
||||||
|
return Gtk.accelerator_get_label(keyval, mods)
|
||||||
|
|
||||||
|
|
||||||
|
def _itemSettings(customId):
|
||||||
|
return Gio.Settings.new_with_path(ITEM_SCHEMA, ITEM_PATH_ROOT + customId + "/")
|
||||||
|
|
||||||
|
|
||||||
|
def listShortcuts():
|
||||||
|
listSettings = Gio.Settings.new(LIST_SCHEMA)
|
||||||
|
ids = list(listSettings.get_strv("custom-list"))
|
||||||
|
shortcuts = []
|
||||||
|
for customId in ids:
|
||||||
|
item = _itemSettings(customId)
|
||||||
|
binding = item.get_strv("binding")
|
||||||
|
shortcuts.append(Shortcut(
|
||||||
|
customId,
|
||||||
|
item.get_string("name"),
|
||||||
|
item.get_string("command"),
|
||||||
|
binding[0] if binding else "",
|
||||||
|
))
|
||||||
|
return shortcuts
|
||||||
|
|
||||||
|
|
||||||
|
def _existingIndices():
|
||||||
|
listSettings = Gio.Settings.new(LIST_SCHEMA)
|
||||||
|
indices = set()
|
||||||
|
for customId in listSettings.get_strv("custom-list"):
|
||||||
|
if customId.startswith("custom") and customId[6:].isdigit():
|
||||||
|
indices.add(int(customId[6:]))
|
||||||
|
try:
|
||||||
|
raw = subprocess.run(
|
||||||
|
["dconf", "list", ITEM_PATH_ROOT],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
).stdout
|
||||||
|
for line in raw.splitlines():
|
||||||
|
entry = line.strip().rstrip("/")
|
||||||
|
if entry.startswith("custom") and entry[6:].isdigit():
|
||||||
|
indices.add(int(entry[6:]))
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
return indices
|
||||||
|
|
||||||
|
|
||||||
|
def _nextCustomId():
|
||||||
|
used = _existingIndices()
|
||||||
|
nextIndex = 0
|
||||||
|
while nextIndex in used:
|
||||||
|
nextIndex += 1
|
||||||
|
return f"custom{nextIndex}"
|
||||||
|
|
||||||
|
|
||||||
|
def addShortcut(name, command, binding):
|
||||||
|
customId = _nextCustomId()
|
||||||
|
item = _itemSettings(customId)
|
||||||
|
item.set_string("name", name)
|
||||||
|
item.set_string("command", command)
|
||||||
|
item.set_strv("binding", [binding])
|
||||||
|
|
||||||
|
listSettings = Gio.Settings.new(LIST_SCHEMA)
|
||||||
|
ids = list(listSettings.get_strv("custom-list"))
|
||||||
|
ids.append(customId)
|
||||||
|
listSettings.set_strv("custom-list", ids)
|
||||||
|
return customId
|
||||||
|
|
||||||
|
|
||||||
|
def updateShortcut(customId, name, command, binding):
|
||||||
|
item = _itemSettings(customId)
|
||||||
|
item.set_string("name", name)
|
||||||
|
item.set_string("command", command)
|
||||||
|
item.set_strv("binding", [binding])
|
||||||
|
|
||||||
|
|
||||||
|
def deleteShortcut(customId):
|
||||||
|
listSettings = Gio.Settings.new(LIST_SCHEMA)
|
||||||
|
ids = [existingId for existingId in listSettings.get_strv("custom-list") if existingId != customId]
|
||||||
|
listSettings.set_strv("custom-list", ids)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["dconf", "reset", "-f", ITEM_PATH_ROOT + customId + "/"],
|
||||||
|
capture_output=True, check=True,
|
||||||
|
)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def findConflicts(binding, excludeCustomId=None):
|
||||||
|
if not binding:
|
||||||
|
return []
|
||||||
|
conflicts = []
|
||||||
|
for shortcut in listShortcuts():
|
||||||
|
if shortcut.customId == excludeCustomId:
|
||||||
|
continue
|
||||||
|
if shortcut.binding == binding:
|
||||||
|
conflicts.append(f"custom shortcut \"{shortcut.name}\"")
|
||||||
|
|
||||||
|
for schemaId in CONFLICT_SCHEMAS:
|
||||||
|
settings = Gio.Settings.new(schemaId)
|
||||||
|
for key in settings.list_keys():
|
||||||
|
value = settings.get_value(key)
|
||||||
|
if value.get_type_string() != "as":
|
||||||
|
continue
|
||||||
|
if binding in value.unpack():
|
||||||
|
conflicts.append(f"system shortcut \"{key}\" ({schemaId})")
|
||||||
|
return conflicts
|
||||||
|
|
||||||
|
|
||||||
|
def keyEventToAccelerator(keyval, state):
|
||||||
|
keyvalName = Gtk.accelerator_name(keyval, 0)
|
||||||
|
if keyvalName in IGNORED_KEYVALS:
|
||||||
|
return None
|
||||||
|
mask = state & Gtk.accelerator_get_default_mod_mask()
|
||||||
|
accel = Gtk.accelerator_name(keyval, mask)
|
||||||
|
if not accel or not Gtk.accelerator_valid(keyval, mask):
|
||||||
|
return None
|
||||||
|
return accel
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Quick-fill command presets for the shortcut manager's "new shortcut" dialog.
|
||||||
|
Only offered when the underlying binary is actually installed, so the list
|
||||||
|
never suggests a dead command. Purely a convenience for the command entry -
|
||||||
|
any preset is still just a plain command string once saved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
_CANDIDATES = [
|
||||||
|
("Terminal", "gnome-terminal", "gnome-terminal"),
|
||||||
|
("File manager", "nemo", "nemo"),
|
||||||
|
("Lock screen", "cinnamon-screensaver-command", "cinnamon-screensaver-command --lock"),
|
||||||
|
("Screenshot - area", "flameshot", "flameshot gui"),
|
||||||
|
("Screenshot - full screen", "gnome-screenshot", "gnome-screenshot"),
|
||||||
|
("Web browser", "xdg-open", "xdg-open http://"),
|
||||||
|
("Mute volume toggle", "amixer", "amixer -q set Master toggle"),
|
||||||
|
("Text editor", "gedit", "gedit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def availablePresets():
|
||||||
|
return [(label, command) for label, binary, command in _CANDIDATES if shutil.which(binary)]
|
||||||
Executable
+249
@@ -0,0 +1,249 @@
|
|||||||
|
#!/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()
|
||||||
Reference in new issue
Block a user