Actions/Shortcuts
Menus should have actions with associated shortcut
Some defaults include Quit / Open / Save / Copy / Paste / ...
I'm on KDE and I'm running the app, I've tried to quit the application with ctrl+Q but it doesn't close. I have 2 questions:
- How should this work? Is it platform specific?
- What's the current way to handle this in slint? should one use
FocusScope?
Thanks!
@woile: It should work, but it is not yet implemented :-/
(Translateable) accelerators would be great, too;
Like in Qt: "&File" in Menu, means it's activated by 'F'
In Slint 1.13, if you have a global FocusScope on your window, one can add the capture-key-pressed event and change shortcut there.
What we'd like to do in future version is the following:
- [ ] Add a
keyboard-shortcutbuiltin type, that can be assigned with something like@keys(shift + control + a)(WIP in https://github.com/slint-ui/slint/pull/8906) - [ ] Add a
shortcutproperty to MenuItem, so one can do
export MainWindow inherits Window {
MenuBar {
Menu {
title: @tr("File");
MenuItem {
title: @tr("Open");
shortcut: @keys(Ctrl+O);
activated => { /*...*/ }
}
}
}
}
One important thing of shortcuts is to be dependent on the current focus. For example if the focus is in a TextInput, we may want the shortcut Ctrl+Z to undo the last modification just in that text input. But if the focus is not in the TextInput, the shortcut event should bubble up to its parents until one of them captures the same shortcut for another action (e.g. Ctrl+Z would then undo changes in the opened document instead).
Somehow I am thinking about a Shortcut element like this:
export MainWindow inherits Window {
// Global shortcuts
Shortcut {
shortcut: @keys(Ctrl+Z);
activated => { /* undo changes in document */ }
}
// Somewhere in a child widget, e.g. in a properties sidebar of selected objects
TextInput {
Shortcut {
shortcut: @keys(Ctrl+Z);
activated => { /* undo changes in this text input */ }
}
}
}
Just my 2 cents without thinking too much about it yet, so I'm not sure if that makes sense.
Hello everybody, I'm very excited about the future improvements around keyboard shortcuts ! I've been developing a commercial DAW (Digital Audio Workstation, a software to produce music) from scratch using Rust and Slint since February, and I think it could help the discussion if I shared my concerns and ideas, and the way I implement shortcuts in my application. English is not my main language, so I hope I'll be understandable.
My application, as almost every DAW, needs to have editable shortcuts. Currently the shortcuts are stored as an HashMap in Rust (which in the future will be imported from a shortcut file), and actions are triggered through a callback in my main_window : the information about the keys pressed are sent through the callback, and the whole logic of finding the right action to trigger in the HashMap is managed in Rust. This is very flexible and ideal for me, since my keyboard shortcuts need to be able to trigger either built-in actions (Add a track, mute a region..) or a user-defined macro. This is standard in DAWs, and I'm wondering if this new keyboard shortcuts system could fit with my current specification.
The problem I have to deal with in the current implementation is displaying the right shortcuts dynamically ; I think I will generate a Struct from the Hashmap whenever the user changes a shortcut, in a format readable by Slint, that associates unique ids and their corresponding shortcuts. Then I'll store it globally, and get the shortcuts where I need to display them by getting the corresponding field of the struct.
Third, I don't use Menus at all ; as far as I know, their appearance is not customizable (correct me if I'm wrong !), and since design is VERY important in this project and one of my main concerns, I cannot use them at the moment. I'll have to implement my own menu in the future, to have total control over it, so I think I will not benefit from the Menu improvements. I hope that one day Menus will be as customizable and free as the rest of the application, both the menus themselves and their content !With a fallback for Apple, where the menu is displayed at the top, separate from the application.
These are my concerns, hope this helps with the discussion !
TextInput {
Shortcut {
shortcut: @keys(Ctrl+Z);
activated => { /* undo changes in this text input */ }
}
}
Personally, I think code like this should only exist during the test-phase or in applications that aren't used by more than one person. It makes it inviting to use hardcoded shortcuts, which should always be discouraged.
I suggest something more like this:
import { SystemShortcuts } from "system-defaults.slint"
export global Shortcuts {
// these should be considered defaults, to be overriden by the user
in-out property my_action: [@keys(Ctrl+A), @keys(Ctrl+F)]; // support multiple shortcuts per action
in-out property my_multi_key_action: @keys(Ctrl+K,A) // press Ctrl+K, release, press A
}
TextInput {
Shortcut {
shortcut: SystemShortcuts.undo;
activated => { /* undo changes in this text input */ }
}
}
Ideally there would be an easy way to generate code for a Dialog that allows the user to change shortcuts from the Shortcuts global (depending on OS/Desktop Environment - e.g use KDE shortcut dialog if possible on KDE). In that, shortcuts that can not appear in the same scope, should not be treated as conflicting.
export MainWindow inherits Window {
// Global shortcuts
Do you really mean Global shortcuts, or application-wide ones? Both should be possible. And what about shortcuts that can only be used in a specific window? Putting them there could apply to any of those.
Personally, I think code like this should only exist during the test-phase or in applications that aren't used by more than one person. It makes it inviting to use hardcoded shortcuts, which should always be discouraged.
Of course we need dynamic shortcuts, I just made the example with a hardcoded shortcut for simplicity. In fact we already use dynamic shortcuts at LibrePCB (we call them EditorCommand as they also contain text, icon & status-tip in addition to the shortcut):
https://github.com/LibrePCB/LibrePCB/blob/2e7a82f902ece115c40d151abfcf196b1b878d95/libs/librepcb/editor/ui/api/editorcommandset.slint
All those shortcuts are dynamically loaded from a file at runtime and can be configured.
And yes, multiple shortcuts for the same action is also important for us (e.g. Ctrl+Y and Ctrl+Shift+Z for "redo").
Do you really mean Global shortcuts, or application-wide ones?
I meant window-global shortcuts.
My mind spun around this before and right after sleep, making me realize that my previous suggestion is far from sufficient.
Of course we need dynamic shortcuts, I just made the example with a hardcoded shortcut for simplicity. In fact we already use dynamic shortcuts at LibrePCB (we call them EditorCommand as they also contain text, icon & status-tip in addition to the shortcut):
I just took your example to stress the importance of making this configurable ;).
The EditorCommand looks like a good direction. I haven't even thought about icons, but those are really needed there, too.
So based on my thoughts and EditorCommand I would now suggest this:
export struct Command {
icon: Image,
text: string,
status-tip: string,
shortcuts: [KeyCombo],
category: string,
scope-hint: string,
}
text-- Name to be shown in menu, etc.status-tip-- I assume this is for the Tooltip. Could default totextif empty.category-- The category to show in when configuring shortcuts. If the command appears in a menu, can we make this default to the Menu-text?scope-hint-- additional info, shown in parenthesis, in the shortcut configuration dialog. Used to differentiate from similiar actions that are used in different scopes when having individual shortcuts is desireble.
Then SystemShortcuts.undo should be a Command.
Further thoughts
Conflicts
When using a mix of system and application-defined shortcuts, conflicts will happen. This is another reason why making configuring them as easy as possible would be great. There should also be a way to override system shortcuts within the application. And: what to do in a conflict? KDE shows a message notifying the user about the conflict.
Defaults, languages and layouts
When deciding on reasonable defaults, one big question is: positional or mnemonic?
An example for positional shortcuts is Ctrl-[C,X,V] for copy, cut, paste which are supposed to be in an easy to reach position, beside each other (practice differs - but I assume that was the original thought).
Ctrl+O for "open" is a mnemonic shortcut - starting with the first letter of the action.
To do this right, positional shortcuts need to reference the position of keys and thus be defined independent of the users keyboard layout. Personally I prefer the XKB hardware ids (e.g. AE04 for fifth (E) row from the bottom, fourth key), but USB-Hardware-Keycodes might be a better platform-independent choice. This will still fail for some setups (like mine) - but at least it could work for most users. From what I read, implementing this might be challenging on MacOS.
Mnemonic shortcuts on the other hand, are dependent on the language and by this on the translation of the commands name.
Mixing both is a recipe for conflicts.
I don't really know how to handle this in a reasonable way - but it would be great if we could.
I think we shouldn't make it too complicated here. All we need from Slint is a simple way of catching keyboard shortcuts that will trigger a callback. All the rest (configuration, handling of conflicts, icons, texts, ...) is up to the application, not Slint's job IMHO. As long as shortcuts can be defined as properties (and allow to assign a list of shortcuts), runtime configuration and multi-shortcuts will be possible, so that is all we need. Basically something similar to QShortcut.
Whenever Slint devs decide to also provide some higher-level features (like QAction), that would be fine too, but the low-level building blocks are more important for now.
I agree that the basics are the important thing right now. But I think, making them in a way where my previous suggestion could be implemented later (maybe as a 3rd-party crate) without major changes to slint or the application would be great.
Also remembered one big thing regarding shortcuts: How do you deal with shortcuts that shift the character.
Example: Ctrl+Shift+1? 1 can be accessed with or without Shift, depending on the keyboard layout. Will slint work with the key containing 1 on both layouts? This has long been a major pain point with many applications.
So, how do you handle key codes vs. character input? And how do you handle translations/different layouts? And what’s the list of key names the macro knows?
For example, I’m using a German keyboard layout (E2, the latest standard). I have separate keys for ÄÖÜß, the so-called ISO key (for <>| in T2-3, Ex/Lv5 Shift in E2), a ^ key, etc. How would i name those keys as a German dev for German layouts? @keys(Control + ß)? What will it look like when a translator inputs an alternative in their own layout, like @keys(Control + É)? And speaking of translations: What about aliases like RAlt vs. Alt Gr (important difference), or Ctrl vs. Strg (not so important one)? My quick fridge thought suggestions is: Use 'x' syntax for character codes, non-'' syntax for positional codes, '' as escape for a ' key. Prefix positional codes with e.g. us-, like us-Z to make mistakes less likely. E.g. Control + 'Z' vs. Control + us-Z. And add the Alt Gr modifier, which sometimes is RAlt, and the us-ISO key. This could also be used to handle cases where sometimes apps want numpad and numrow keys to alias, but other times not. The former you’d catch with '1', the latter with us-1 vs. us-KP1.
(As for the importance of being able to express us-Z vs. 'Z': Consider WASD and QWERTZ vs. QWERTY vs. completely different things like Neo2.)
Speaking of Ctrl+'Z': Can menu items be extended for alternative shortcuts? Usually 1 alternative suffices. One of the most common alternatives is that in some apps, redo is Ctrl+Y, in others it’s Ctrl+Shift+Z. Many apps support both for that reason. See the Qt docs, which have a table of common shortcuts, columns for common OSs and DEs, and many cells have multiple shortcuts assigned. KDE even lets you have a practically infinite amount of aliases for common actions. In other words: It would be great if e.g. menu items didn’t just take a single shortcut, but an ordered list of shortcuts.
On that note, can we expect key combo files, akin to translation files, where we map shortcuts to different keyboard layouts? Like, where with @tr we pick our text for de-DE, en-GB, etc., we’d fetch our keyboard shortcuts from de-T2, de-e2, fr-bepo, etc. files? I would, similar to BCP-47 language negotiation, expect a de shortcut map to act as catch-all fallback for all de-* layouts, etc. And i’d also expect to be able to bundle key map files with the app, like we can do with translations.
Finally, i assume we’ll be getting a special new input widget for keyboard shortcuts, so we can build shortcut config menus? Desirable and common features of such a widget are: Esc aborts shortcut capture, leaving the setting empty or clearing it, other shortcuts are all intercepted, so you can double-assign already existing shortcuts without unintentionally triggering the associated action, and tracking whether the current shortcut is already active anywhere else in the current window’s component tree so we can warn/error on conflicts.
I like @kermitfrog's idea of a Shortcut element. I've found any approach that puts shortcut keybind definitions outside of the code in an easily modifiable text file structure pleasant to work with. It should be intuitive to use if shortcut consumption is handled the same way as Key Handling, bottom-up. I would only expect Slint to be able load a shortcuts file and hook up well formatted entries to valid .slint Shortcuts. Conflicting shortcuts within a focus scope would just be determined in last-in-first-out order.
myApp.slint
export MainWindow inherits Window {
MenuBar {
Menu {
title: @tr("File");
MenuItem {
title: @tr("Open");
// How I would expect find the feature under QML
shortcut.name: "open_file" //plain text may not be a good options but it's a straightforward idea
shortcut.activated => { ... }
}
// What I might expect on components without a Shortcut element
Shortcut {
name: SystemShortcuts.focus_menu;
//OR name: "focus_menu"
activated => { ... }
}
}
}
}
shortcuts.txt (case insensitive)
[global_menu]
ctrl-o open_file
ctrl-f1 focus_menu
Where it should be simple enough for an app developer to make a second user_shortcuts.txt to override the defaults by using conflicting shortcut ordering.
Access Keys or Access Text
In Windows, an access key is an underlined letter of the caption of a control such as a button or label where, if the user presses Alt + the access key letter, the action associated with the control is executed. Unfortunately, Microsoft long ago changed the default for Windows setting 'Underline Access Keys: Access keys will be underlined even when not holding Alt' to Off. So most users probably do not know the feature exists. However, for those in the know, and in those applications that have implemented it, the access keys feature provides intuitive keyboard shortcuts that can reduce the health risks associated with mouse overuse.
I don't know whether there are equivalents to access keys in macOS or Linux. However, Avalonia, a cross-platform GUI API for .NET, includes a similar feature called AccessText that should work on all those operating systems. Unlike access keys in Windows, the letter in Avalonia's AccessText stays underlined in Windows regardless of what Underline Access Keys is set to in Windows settings. That is a big plus IMHO. It would be great if access text or access keys or similar were to be implemented in Slint.
Here are a couple of Avalonia AccessText examples:
<CheckBox
IsChecked="{Binding UseControlSurface}">
<AccessText
Text="_Use Control Surface" />
</CheckBox>
<Label
Target="ControlSurfaceComboBox">
<AccessText
Text="_Select a Control Surface MIDI Input" />
</Label>
<ComboBox
x:Name="ControlSurfaceComboBox"
ItemsSource="{Binding MidiInputs}"
SelectedItem="{Binding ControlSurface}" />
With a GUI as implemented in the above code, "U" is underlined in the CheckBox's text and "S" is underlined in the Label's text, as specified by the "_"s. If the user presses Alt+U, the CheckBox is toggled on or off. If the user presses Alt+S, the ComboBox is focused. For that to work, the Label needs not only an AccessText but also a Target property that points to the control to be focused: this feature is specific to Label.
If Slint is to implement access text / access keys, an equivalent of Label will be needed.
@SimonORorke Great addition to have, yes, that’s definitely needed as well.
I don't know whether there are equivalents to access keys in macOS or Linux.
Both, GTK and Qt have that as a built-in feature, which in turn means a ton of Linux apps have it, too. The notation may differ. iirc in Qt you prefix the menu shortcut with &, but that’s potato-tomato.
@Evrey, that sounds very encouraging.