Skip to content

Conversation

@bhennion
Copy link
Collaborator

@bhennion bhennion commented Dec 7, 2024

The long term goal (for a follow up PR) is to implement configurable shortcuts.

This PR simply uses the KeyBinding architecture used in TextEditor for key bindings various locations in the code.

  • Shortcuts in XournalView
  • Shortcuts in SplineHandler
  • Shortcuts in GeometryToolInputHandler
  • Shortctus in BaseShapeHandler
  • Shortcuts in SearchBar

It does not mean all of those key bindings should be open to configuration, but it gives us a common base to work with.

@bhennion
Copy link
Collaborator Author

bhennion commented Dec 9, 2024

In order to get the right implementation, I've been giving some thoughts to shortcuts customization. I need some opinions as to what key binding should be configurable (Note that not all keybindings are "shortcuts" like Ctrl+Z. Some are just "when the user presses Ctrl" or things like that).
To help with the discussion, I put a list of current key bindings defined in the code (it does not contain those defined in mainmenubar.xml) at the bottom of this post.

Now, I think we want everything defined in mainmenubar.xml to be configurable - or possibly even every action in Action.enum.h should be able to be triggered by a configurable shortcut.
But there are other places.
If we look for instance at the key bindings defined in XournalView, some non-standard shortcuts (from vim) have been hardcoded. We should definitely not have hardcoded those but it shows there is a demand for customization outside of the main menubar's entries.

IMO, the shortcut customization dialog should be aware of almost all key bindings accross the app: it should be able to detect conflicts.

The other thing to consider is: if a key binding is customizable, then it improves its discoverability (just because it has to be written down in the customization dialog). I don't think a lot of users know about the non-trivial key bindings of the geometry tools/spline handler/shape handlers/vertical tool.

My feeling is that:

  • Key bindings defined in mainmenubar.xml and in control/actions/ActionProperties.h should be configurable. The user should be able to add/modify shortcuts for most (if not all) of the actions in the ActionDatabase (with a choice of parameter if the GAction has a parameter)
  • Key bindings in BaseShapeHandler could be configurable: improves discoverability
  • Key bindings in SplineHandler should be configurable: allows to reassign "single key shortcuts" to other uses (see e.g. Single key shortcuts ? #6115) + improves discoverability
  • Key bindings in VerticalToolHandler could be configurable (improves discoverability)
  • Key bindings in EditSelection: I don't know. Maybe not configurable
  • Key bindings in TextEditor shoud not be configurable: the keyboard input is too important to let users meddle with it here (like introducing a single key shortcut for instance...).
  • Key bindings in GeometryToolInputHandler should be configurable: allows to reassign "single key shortcuts" to other uses (see e.g. Single key shortcuts ? #6115) + improves discoverability
  • Key bindings in SearchBar should not be configurable
  • Key bindings in XournalView: I would have said "don't allow for customization" but as I mentionned above, contributors have already customized them (by adding vim-like navigation). So maybe yes, or maybe partially, or maybe the navigation things should be triggered via the ActionDatabase and the color things should be configurable

So, @xournalpp/core do you have any opinion, insights or comments about those questions?

List of current keybindings:

Key bindings in BaseShapeHandler

    if (event.keyval == GDK_KEY_Shift_L || event.keyval == GDK_KEY_Shift_R) {
59:    } else if (event.keyval == GDK_KEY_Control_L || event.keyval == GDK_KEY_Control_R) {
61:    } else if (event.keyval == GDK_KEY_Alt_L || event.keyval == GDK_KEY_Alt_R) {

Key bindings in SplineHandler

61:        case GDK_KEY_BackSpace: {
72:        case GDK_KEY_Right: {
77:        case GDK_KEY_Left: {
82:        case GDK_KEY_Up: {
87:        case GDK_KEY_Down: {
92:        case GDK_KEY_r:
93:        case GDK_KEY_R: {  // r like "rotate"
94:            double angle = (event.keyval == GDK_KEY_R) ? -ROTATE_AMOUNT : ROTATE_AMOUNT;
103:        case GDK_KEY_s:
104:        case GDK_KEY_S: {  // s like "scale"
109:            if (event.keyval == GDK_KEY_S) {
131:    if (event.keyval == GDK_KEY_Escape) {

Key bindings in VerticalToolHandler

71:    if ((event.keyval == GDK_KEY_Control_L || event.keyval == GDK_KEY_Control_R) && this->spacingSide == Side::Below) {
80:    if ((event.keyval == GDK_KEY_Control_L || event.keyval == GDK_KEY_Control_R) && this->spacingSide == Side::Above) {

Key bindings in EditSelection

1255:            {{{NONE,  GDK_KEY_Left},    moveAndKeepVisible<-REGULAR_MOVE_AMOUNT,                    0>},
1256:             {{ALT,   GDK_KEY_Left},    moveAndKeepVisible<  -SMALL_MOVE_AMOUNT,                    0>},
1257:             {{SHIFT, GDK_KEY_Left},    moveAndKeepVisible<  -LARGE_MOVE_AMOUNT,                    0>},
1258:             {{NONE,  GDK_KEY_Right},   moveAndKeepVisible< REGULAR_MOVE_AMOUNT,                    0>},
1259:             {{ALT,   GDK_KEY_Right},   moveAndKeepVisible<   SMALL_MOVE_AMOUNT,                    0>},
1260:             {{SHIFT, GDK_KEY_Right},   moveAndKeepVisible<   LARGE_MOVE_AMOUNT,                    0>},
1261:             {{NONE,  GDK_KEY_Up},      moveAndKeepVisible<                   0, -REGULAR_MOVE_AMOUNT>},
1262:             {{ALT,   GDK_KEY_Up},      moveAndKeepVisible<                   0,   -SMALL_MOVE_AMOUNT>},
1263:             {{SHIFT, GDK_KEY_Up},      moveAndKeepVisible<                   0,   -LARGE_MOVE_AMOUNT>},
1264:             {{NONE,  GDK_KEY_Down},    moveAndKeepVisible<                   0,  REGULAR_MOVE_AMOUNT>},
1265:             {{ALT,   GDK_KEY_Down},    moveAndKeepVisible<                   0,    SMALL_MOVE_AMOUNT>},
1266:             {{SHIFT, GDK_KEY_Down},    moveAndKeepVisible<                   0,    LARGE_MOVE_AMOUNT>},
1267:             {{NONE,  GDK_KEY_Escape},  clear}});

Key bindings in TextEditor

21:    {{mod, GDK_KEY_##key}, wrap<&TextEditor::moveCursor, mvt, dir, false>},                \
22:            {{mod & SHIFT, GDK_KEY_##key}, wrap<&TextEditor::moveCursor, mvt, dir, true>}, \
23:            {{mod, GDK_KEY_KP_##key}, wrap<&TextEditor::moveCursor, mvt, dir, false>}, {   \
24:        {mod& SHIFT, GDK_KEY_KP_##key}, wrap<&TextEditor::moveCursor, mvt, dir, true>      \
48:         {{CTRL, GDK_KEY_a}, wrap<&TextEditor::selectAtCursor, TextEditor::SelectType::ALL>},
49:         {{CTRL, GDK_KEY_slash}, wrap<&TextEditor::selectAtCursor, TextEditor::SelectType::ALL>},
51:         {{NONE, GDK_KEY_Delete}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_CHARS, 1>},
52:         {{NONE, GDK_KEY_KP_Delete}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_CHARS, 1>},
54:         {{NONE, GDK_KEY_BackSpace}, wrap<&TextEditor::backspace>},
55:         {{SHIFT, GDK_KEY_BackSpace}, wrap<&TextEditor::backspace>},
57:         {{CTRL, GDK_KEY_Delete}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_WORD_ENDS, 1>},
58:         {{CTRL, GDK_KEY_KP_Delete}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_WORD_ENDS, 1>},
59:         {{CTRL, GDK_KEY_BackSpace}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_WORD_ENDS, -1>},
61:         {{CTRL & SHIFT, GDK_KEY_Delete}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_PARAGRAPH_ENDS, 1>},
62:         {{CTRL & SHIFT, GDK_KEY_KP_Delete}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_PARAGRAPH_ENDS, 1>},
63:         {{CTRL & SHIFT, GDK_KEY_BackSpace}, wrap<&TextEditor::deleteFromCursor, GTK_DELETE_PARAGRAPH_ENDS, -1>},
65:         {{CTRL, GDK_KEY_x}, wrap<&TextEditor::cutToClipboard>},
66:         {{SHIFT, GDK_KEY_Delete}, wrap<&TextEditor::cutToClipboard>},
68:         {{CTRL, GDK_KEY_c}, wrap<&TextEditor::copyToClipboard>},
69:         {{CTRL, GDK_KEY_Insert}, wrap<&TextEditor::copyToClipboard>},
71:         {{CTRL, GDK_KEY_v}, wrap<&TextEditor::pasteFromClipboard>},
72:         {{SHIFT, GDK_KEY_Insert}, wrap<&TextEditor::pasteFromClipboard>},
74:         {{NONE, GDK_KEY_Insert}, wrap<&TextEditor::toggleOverwrite>},
75:         {{NONE, GDK_KEY_KP_Insert}, wrap<&TextEditor::toggleOverwrite>},
77:         {{CTRL, GDK_KEY_b}, wrap<&TextEditor::toggleBoldFace>},
78:         {{CTRL, GDK_KEY_B}, wrap<&TextEditor::toggleBoldFace>},
80:         {{CTRL, GDK_KEY_plus}, wrap<&TextEditor::increaseFontSize>},
81:         {{CTRL, GDK_KEY_KP_Add}, wrap<&TextEditor::increaseFontSize>},
83:         {{CTRL, GDK_KEY_minus}, wrap<&TextEditor::decreaseFontSize>},
84:         {{CTRL, GDK_KEY_KP_Subtract}, wrap<&TextEditor::decreaseFontSize>},
86:         {{NONE, GDK_KEY_Return}, wrap<&TextEditor::linebreak>},
87:         {{NONE, GDK_KEY_ISO_Enter}, wrap<&TextEditor::linebreak>},
88:         {{NONE, GDK_KEY_KP_Enter}, wrap<&TextEditor::linebreak>},
90:         {{NONE, GDK_KEY_Tab}, wrap<&TextEditor::tabulation>},
91:         {{NONE, GDK_KEY_KP_Tab}, wrap<&TextEditor::tabulation>},
92:         {{NONE, GDK_KEY_ISO_Left_Tab}, wrap<&TextEditor::tabulation>},
94:         {{SHIFT, GDK_KEY_Tab}, wrap<&TextEditor::tabulation>},
95:         {{SHIFT, GDK_KEY_KP_Tab}, wrap<&TextEditor::tabulation>},
96:         {{SHIFT, GDK_KEY_ISO_Left_Tab}, wrap<&TextEditor::tabulation>}});

Key bindings in GeometryToolInputHandler

140:        case GDK_KEY_Left:
143:        case GDK_KEY_Up:
146:        case GDK_KEY_Right:
149:        case GDK_KEY_Down:
152:        case GDK_KEY_r:
155:        case GDK_KEY_R:  // r like "rotate"
158:        case GDK_KEY_s:
161:        case GDK_KEY_S:
164:        case GDK_KEY_m:

Key bindings in SearchBar

37:                         if (event->keyval == GDK_KEY_Return) {
46:                         } else if (event->keyval == GDK_KEY_Escape) {

Key bindings in XournalView

31:        {{NONE, GDK_KEY_Page_Down},    wrap<&ScrollHandler::goToNextPage>},
32:        {{NONE, GDK_KEY_KP_Page_Down}, wrap<&ScrollHandler::goToNextPage>},
33:        {{NONE, GDK_KEY_Page_Up},      wrap<&ScrollHandler::goToPreviousPage>},
34:        {{NONE, GDK_KEY_KP_Page_Up},   wrap<&ScrollHandler::goToPreviousPage>},
35:        {{NONE, GDK_KEY_space},        wrap<&ScrollHandler::scrollByVisibleArea, ScrollHandler::DOWN>},
36:        {{SHIFT, GDK_KEY_space},       wrap<&ScrollHandler::scrollByVisibleArea, ScrollHandler::UP>},
38:        {{SHIFT, GDK_KEY_KP_Up},       wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::UP>},
39:        {{SHIFT, GDK_KEY_Up},          wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::UP>},
40:        {{SHIFT, GDK_KEY_k},           wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::UP>},  // Why??
41:        {{SHIFT, GDK_KEY_K},           wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::UP>},  // Why??
42:        {{SHIFT, GDK_KEY_KP_Down},     wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::DOWN>},
43:        {{SHIFT, GDK_KEY_Down},        wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::DOWN>},
44:        {{SHIFT, GDK_KEY_j},           wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::DOWN>},  // Why??
45:        {{SHIFT, GDK_KEY_J},           wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::DOWN>},  // Why??
46:        {{SHIFT, GDK_KEY_KP_Left},     wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::LEFT>},
47:        {{SHIFT, GDK_KEY_Left},        wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::LEFT>},
48:        {{SHIFT, GDK_KEY_h},           wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::LEFT>},  // Why??
49:        {{SHIFT, GDK_KEY_KP_Right},    wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::RIGHT>},
50:        {{SHIFT, GDK_KEY_Right},       wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::RIGHT>},
51:        {{SHIFT, GDK_KEY_l},           wrap<&ScrollHandler::scrollByOnePage, ScrollHandler::RIGHT>},  // Why??
53:        {{NONE, GDK_KEY_KP_Up},        wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::UP>},
54:        {{NONE, GDK_KEY_Up},           wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::UP>},
55:        {{NONE, GDK_KEY_k},            wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::UP>},  // Why??
56:        {{NONE, GDK_KEY_K},            wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::UP>},  // Why??
57:        {{NONE, GDK_KEY_KP_Down},      wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::DOWN>},
58:        {{NONE, GDK_KEY_Down},         wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::DOWN>},
59:        {{NONE, GDK_KEY_j},            wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::DOWN>},  // Why??
60:        {{NONE, GDK_KEY_J},            wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::DOWN>},  // Why??
61:        {{NONE, GDK_KEY_KP_Left},      wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::LEFT>},
62:        {{NONE, GDK_KEY_Left},         wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::LEFT>},
63:        {{NONE, GDK_KEY_h},            wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::LEFT>},  // Why??
64:        {{NONE, GDK_KEY_KP_Right},     wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::RIGHT>},
65:        {{NONE, GDK_KEY_Right},        wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::RIGHT>},
66:        {{NONE, GDK_KEY_l},            wrap<&ScrollHandler::scrollByOneStep, ScrollHandler::RIGHT>},  // Why??
68:        {{NONE, GDK_KEY_End},          wrap<&ScrollHandler::goToLastPage>},
69:        {{NONE, GDK_KEY_KP_End},       wrap<&ScrollHandler::goToLastPage>},
70:        {{NONE, GDK_KEY_Home},         wrap<&ScrollHandler::goToFirstPage>},
71:        {{NONE, GDK_KEY_KP_Home},      wrap<&ScrollHandler::goToFirstPage>}
86:        {{NONE, GDK_KEY_1}, setColorByNumber<0>},
87:        {{NONE, GDK_KEY_2}, setColorByNumber<1>},
88:        {{NONE, GDK_KEY_3}, setColorByNumber<2>},
89:        {{NONE, GDK_KEY_4}, setColorByNumber<3>},
90:        {{NONE, GDK_KEY_5}, setColorByNumber<4>},
91:        {{NONE, GDK_KEY_6}, setColorByNumber<5>},
92:        {{NONE, GDK_KEY_7}, setColorByNumber<6>},
93:        {{NONE, GDK_KEY_8}, setColorByNumber<7>},
94:        {{NONE, GDK_KEY_9}, setColorByNumber<8>},
95:        {{NONE, GDK_KEY_0}, setColorByNumber<9>},

@rolandlo
Copy link
Member

I haven't made up my mind yet, about which shortcuts/keypresses should be configurable yet. But about discoverability let me add that there is another way to help with that: the GtkShortcutsWindow (which is present both in Gtk3 and Gtk4). It's just used for presenting shortcuts/keypresses, not for modifying them.
In the Libadwaita redesign mockup I have added a GtkShortcutsWindow with all shortcuts I could find. For example here is the section about the geometry tools:
Bildschirmfoto vom 2024-12-10 05-35-02

@bhennion
Copy link
Collaborator Author

I haven't made up my mind yet, about which shortcuts/keypresses should be configurable yet. But about discoverability let me add that there is another way to help with that: the GtkShortcutsWindow (which is present both in Gtk3 and Gtk4).

That's a good point, but I am not quite sure how usable it is with configurable shortcuts (even just for displaying them). It seems that GtkShortcutsShortcut/GtkShortcutsSection/GtkShortcutsWindow have no public constructor. The only way to build them is via a GtkBuilder and a UI file. It makes it complicated to generate a GtkShortcutsWindow dynamically.

@bhennion
Copy link
Collaborator Author

About what should be configurable or not: we will probably want to have some immutable shortcuts that the user cannot override, while allowing the user to add another shortcut for the same action. I have in mind navigation key bindings for instance.

@rolandlo
Copy link
Member

I haven't made up my mind yet, about which shortcuts/keypresses should be configurable yet. But about discoverability let me add that there is another way to help with that: the GtkShortcutsWindow (which is present both in Gtk3 and Gtk4).

That's a good point, but I am not quite sure how usable it is with configurable shortcuts (even just for displaying them). It seems that GtkShortcutsShortcut/GtkShortcutsSection/GtkShortcutsWindow have no public constructor. The only way to build them is via a GtkBuilder and a UI file. It makes it complicated to generate a GtkShortcutsWindow dynamically.

Apparently that changed in Gtk 4.14 though with new API
gtk_shortcuts_window_add_section
gtk_shortcuts_section_add_group
gtk_shortcuts_group_add_shortcut , see
https://docs.gtk.org/gtk4/class.ShortcutsSection.html?q=shortcuts

It's unclear to me however how to construct a GtkShortcutsShortcut though. In Python I can do Gtk.ShortcutsShortcut() to construct one, but I don't see how to set its properties.

@rolandlo
Copy link
Member

I haven't made up my mind yet, about which shortcuts/keypresses should be configurable yet. But about discoverability let me add that there is another way to help with that: the GtkShortcutsWindow (which is present both in Gtk3 and Gtk4).

That's a good point, but I am not quite sure how usable it is with configurable shortcuts (even just for displaying them). It seems that GtkShortcutsShortcut/GtkShortcutsSection/GtkShortcutsWindow have no public constructor. The only way to build them is via a GtkBuilder and a UI file. It makes it complicated to generate a GtkShortcutsWindow dynamically.

Apparently that changed in Gtk 4.14 though with new API gtk_shortcuts_window_add_section gtk_shortcuts_section_add_group gtk_shortcuts_group_add_shortcut , see https://docs.gtk.org/gtk4/class.ShortcutsSection.html?q=shortcuts

It's unclear to me however how to construct a GtkShortcutsShortcut though. In Python I can do Gtk.ShortcutsShortcut() to construct one, but I don't see how to set its properties.

I see how this is supposed to be done now (in Python). This works in the Libadwaita redesign mockup:

        shortcut_window = win.get_help_overlay()
        section = Gtk.ShortcutsSection()
        section.props.title = "My new section"
        shortcut_window.add_section(section)
        group = Gtk.ShortcutsGroup()
        group.props.title = "My new group"
        section.add_group(group)
        shortcut = Gtk.ShortcutsShortcut()
        shortcut.props.accelerator = "<Ctrl><Shift>j"
        shortcut.props.title = "My new shortcut"
        group.add_shortcut(shortcut)

Bildschirmfoto vom 2024-12-11 11-14-06

@bhennion
Copy link
Collaborator Author

I see how this is supposed to be done now (in Python). This works in the Libadwaita redesign mockup:

The C/C++ version of that is probably along the lines

GtkShortcutsShortcut* shortcut = GTK_SHORTCUTS_SHORTCUT(g_object_new(GTK_TYPE_SHORTCUTS_SHORTCUT, "accelerator", "<Ctrl><Shift>j", "title", "My name", nullptr));

In any case, this isn't for now as we need GTK 4.14.

@bhennion
Copy link
Collaborator Author

\action create-installers debian ubuntu-22

@github-actions
Copy link
Contributor

Started 2 build(s) (see details)

@github-actions
Copy link
Contributor

CI successful!
Available artifacts:

@rolandlo
Copy link
Member

My feeling is that:

* Key bindings defined in mainmenubar.xml and in control/actions/ActionProperties.h should be configurable. The user should be able to add/modify shortcuts for most (if not all) of the actions in the ActionDatabase (with a choice of parameter if the GAction has a parameter)

I agree.

* Key bindings in BaseShapeHandler could be configurable: improves discoverability

I agree.

* Key bindings in SplineHandler should be configurable: allows to reassign "single key shortcuts" to other uses (see e.g. [Single key shortcuts ? #6115](https://github.com/xournalpp/xournalpp/issues/6115)) + improves discoverability

I agree.

* Key bindings in VerticalToolHandler could be configurable (improves discoverability)

I agree.

* Key bindings in EditSelection: I don't know. Maybe not configurable

Not configurable, I would say. These are pretty standard and intuitive.

* Key bindings in TextEditor shoud **not** be configurable: the keyboard input is too important to let users meddle with it here (like introducing a single key shortcut for instance...).

I agree.

* Key bindings in GeometryToolInputHandler should be configurable: allows to reassign "single key shortcuts" to other uses (see e.g. [Single key shortcuts ? #6115](https://github.com/xournalpp/xournalpp/issues/6115)) + improves discoverability

I agree.

* Key bindings in SearchBar should not be configurable

I agree.

* Key bindings in XournalView: I would have said "don't allow for customization" but as I mentioned above, contributors have already customized them (by adding vim-like navigation). So maybe yes, or maybe partially, or maybe the navigation things should be triggered via the ActionDatabase and the color things should be configurable

The color things should definitely be configurable. The arrow keys should probably not be configurable, but allowing to add other navigation keys (like vim-style navigation) should be allowed.

So, @xournalpp/core do you have any opinion, insights or comments about those questions?

List of current keybindings:
Key bindings in BaseShapeHandler

Key bindings in SplineHandler

Key bindings in VerticalToolHandler

Key bindings in EditSelection

Key bindings in TextEditor

Key bindings in GeometryToolInputHandler

Key bindings in SearchBar

Key bindings in XournalView

@bhennion
Copy link
Collaborator Author

bhennion commented Aug 3, 2025

And could you have a quick look at the code and tell me if it looks like progress or not? It feels to me it cleans up things quite a bit, in particular by getting rid of the else if (key ==GDK_KEY_??? && state == GDK_MODIFIER_???) { chains.

@rolandlo
Copy link
Member

rolandlo commented Aug 3, 2025

And could you have a quick look at the code and tell me if it looks like progress or not? It feels to me it cleans up things quite a bit, in particular by getting rid of the else if (key ==GDK_KEY_??? && state == GDK_MODIFIER_???) { chains.

Sure, it feels like a big step forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants