Fix Dropdown issue

This commit is contained in:
2026-10-18 20:26:22 +02:00
parent 95f101c0c9
commit 673af1ae43
23 changed files with 2219 additions and 975 deletions

136
AGENTS.md
View File

@@ -26,12 +26,28 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
## i18n — easy to get wrong ## i18n — easy to get wrong
- English is the source language (the `msgid`) **and** also has an explicit - English is the source language (the `msgid`) **and** also has an explicit
identity catalog `po/en.po`. Languages: `de`, `en`, `es`, `fr`. identity catalog `po/en.po`. Languages: `de`, `en`, `es`, `fr`. The workflow,
- `make translations` does **not** regenerate the `.pot`. When you add or change the rules behind it and adding a language are documented in `po/README.md`; the
a UI string, edit `po/gkeymapper.pot` and `po/*.po` by hand, then run commands are `scripts/i18n.sh` (`make pot`, `make i18n-check`,
`make translations`; validate with `msgfmt --check -o /dev/null po/<lang>.po`. `make i18n-status`).
- Never edit the generated `.mo` files under `locale/` (gitignored). Delete any - `make translations` does **not** regenerate the `.pot` (a pot written by a build
`po/*.po~` backups before committing. is a pot nobody reviews). When you add or change a UI string: `make pot`, then
`scripts/i18n.sh update` to merge it into the catalogs, then fill in the msgstrs
in `po/*.po` — and in `po/en.po`, whose msgstr repeats the msgid. `_()` is
translated where it is used, `N_()` is only collected (the help paragraphs in
`src/help.cpp`); the script always passes both keywords to `xgettext`.
- The catalogs carry **no** `#: file:line` comments; the locations live in the pot
only (`update` merges with `msgmerge --no-location`) and
`scripts/i18n.sh todo <lang>` shows them from there. `make i18n-check` fails
when the pot is behind the sources, when a catalog misses a message or a
translation, or when `po/*.po` and `kLanguageCodes[]` (`src/optionsdialog.cpp`)
disagree — a catalog nobody can select is dead weight.
- The daemon is **not** translated (no `setlocale`/`bindtextdomain`, only the GUI
sets gettext up in `main()`); `i18n-check` reports a `_()` marker under
`daemon/`.
- Never edit the generated `.mo` files under `locale/` (gitignored). A manual
`msgmerge --update` leaves `po/*.po~` backups — the script deletes them, by hand
delete them before committing.
- All documentation and source comments are English. German appears only inside - All documentation and source comments are English. German appears only inside
`po/de.po`. `po/de.po`.
@@ -43,8 +59,8 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
app id, the version and the D-Bus names both sides use. The GUI side is app id, the version and the D-Bus names both sides use. The GUI side is
`main.cpp` (window, actions) plus `keyrow.h`, `mappings`, `capturedialog`, `main.cpp` (window, actions) plus `keyrow.h`, `mappings`, `capturedialog`,
`daemonclient`, `singleinstance`, `trayicon` (StatusNotifierItem + dbusmenu), `daemonclient`, `singleinstance`, `trayicon` (StatusNotifierItem + dbusmenu),
`settings` (`gui.conf`), `autostart` (the session's `.desktop` entry), `help` `settings` (`gui.conf`), `autostart` (the session's `.desktop` entry), `help`,
and `optionsdialog`. `optionsdialog` and `popovers` (the popup input grab, see below).
- The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp` - The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp`
+ the generated `build/gresources.c`, see Assets) **and** the daemon + the generated `build/gresources.c`, see Assets) **and** the daemon
(`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`, (`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`,
@@ -120,9 +136,18 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
with `SIGTERM` after checking that the bus name really belongs to a with `SIGTERM` after checking that the bus name really belongs to a
`gkeymapper` process. Only then does the new process become the primary, `gkeymapper` process. Only then does the new process become the primary,
because otherwise `GtkApplication` would hand over to the old process and its because otherwise `GtkApplication` would hand over to the old process and its
window — possibly one from an older build — would stay on screen. Row edits window — possibly one from an older build — would stay on screen.
that were not saved are gone with the replaced window; there is no implicit - The takeover also carries the rows over, so an edit that was typed but never
save. saved is not lost with the replaced window: before the quit,
`replace_running_instance()` asks for them (`Window.GetRows` at
`ipc::kWindowObjectPath`) and `build_ui()` starts the new window from that text
instead of the file. The answering side is `serve_rows_handover()` — called from
`on_activate` **after** `build_ui()`, since that is what fills the rows — with
`mappings_text(rows)`, i.e. the mappings file format, so both ends use
`src/mappings`. An instance that does not answer (an older build) leaves the new
window with the file; asking and serving are best effort and never fatal. The
file itself is untouched until the user presses `Save mappings` — the handover
carries row state, it does not save.
- `on_activate` presents the window that is already there - `on_activate` presents the window that is already there
(`gtk_application_get_windows()`), it does not build a second one: a remote (`gtk_application_get_windows()`), it does not build a second one: a remote
activation — an older build starting next to a new one — would otherwise add a activation — an older build starting next to a new one — would otherwise add a
@@ -168,9 +193,17 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
deleting it. The `Exec` line is the binary that is running now deleting it. The `Exec` line is the binary that is running now
(`g_file_read_link("/proc/self/exe")`, quoted, falls back to the name from (`g_file_read_link("/proc/self/exe")`, quoted, falls back to the name from
`PATH`), so an enabled switch starts the build the user was looking at, and the `PATH`), so an enabled switch starts the build the user was looking at, and the
`Comment` reuses the About text (no second sentence for translators). Only the `Comment` reuses the About text (no second sentence for translators). That
**window** is started; the daemon is a systemd user service and stays untouched `Exec` line also carries `--start-hidden` (`kStartHiddenArg`, taken off the
— the GUI is a convenience, the daemon is the part that has to run. command line in `main()`), so the window is built but stays in the tray: a login
does not put it in front of the user, while a start by hand (no flag) always
shows it. Keeping it hidden is only safe while the panel really hosts the icon,
and only the panel can answer that, asynchronously — so
`on_start_hidden_timeout()` shows the window after `kStartHiddenGraceMs` if the
icon never turned up. A rebuild (language switch) comes up the way the window
was left: `build_ui()` takes the visibility as an argument. Only the **window**
is started; the daemon is a systemd user service and stays untouched — the GUI
is a convenience, the daemon is the part that has to run.
- `Minimize to tray` hides the window on close (`"close-request"` handler) and on - `Minimize to tray` hides the window on close (`"close-request"` handler) and on
minimize. GTK4 has **no** minimize signal and `GdkToplevel` exposes no minimize. GTK4 has **no** minimize signal and `GdkToplevel` exposes no
properties, so `gdk_toplevel_get_state()` is polled every 250 ms while the properties, so `gdk_toplevel_get_state()` is polled every 250 ms while the
@@ -222,6 +255,30 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
`GtkScrolledWindow` (one collapsed the rows earlier). Keep the fixed widths `GtkScrolledWindow` (one collapsed the rows earlier). Keep the fixed widths
(Type dropdown 150 px, target entry 440 px min, action button 180 px) so rows (Type dropdown 150 px, target entry 440 px min, action button 180 px) so rows
do not jump when the language changes. do not jump when the language changes.
- Both dropdowns (Type in the main window, Language in the options window) give
every entry a **trailing space**, added where the model is built — the msgids
stay the bare words. `GtkDropDown` appends the check mark of the selected entry
directly behind the text (`setup_item()` in `gtkdropdown.c`), and without that
space it touches the last letter. The selection is read back by index, never by
the string, so the space is harmless everywhere else.
- `src/popovers.{h,cpp}` takes the input grab off the popovers of a window (the
dropdowns' popup lists and the header bar menu) and closes them itself: on a
click in the window outside the popup, on Escape, and when the window loses the
focus. GTK 4.6 grabs the pointer for an open popover, an X11 grab swallows the
clicks of **every** other client, and this GTK does not close the popup on a
click outside the application either — with one dropdown open nothing else
could be clicked any more, not another program and not the desktop, and the
popup stayed open until Escape (measured with a plain GTK4 probe: the click on
the bare desktop never reached it and `_NET_ACTIVE_WINDOW` did not change; a
click inside the app's own window did close the popup). GTK3 replayed such a
click to the window under the pointer, GTK4 dropped that. `autohide` is what
installs the grab, so switching it off hands the clicks back to the desktop;
the dismissal is then done by hand, which is the rest of the module. It runs
once per toplevel window (`build_ui`, `OptionsDialog::show`), notices popovers
GTK creates later on the press that opens them, and leaves the press on the
popup itself alone — that one is an entry choice and must reach the entry
(recognised by its own surface, not by its position, because GTK reports it in
the popup's coordinates).
- Because it is pure GTK4 it follows the system theme (Mint-Y ships a - Because it is pure GTK4 it follows the system theme (Mint-Y ships a
`gtk-4.0` theme). Do **not** add the `suggested-action` CSS class — Mint-Y `gtk-4.0` theme). Do **not** add the `suggested-action` CSS class — Mint-Y
tints it green and it clashes with the rest of the layout. tints it green and it clashes with the rest of the layout.
@@ -415,7 +472,11 @@ replaced process exits `0` when it was closed through the D-Bus `quit` action, o
instance from an older commit (`git worktree add /tmp/legacy <commit>`), start it instance from an older commit (`git worktree add /tmp/legacy <commit>`), start it
and then start the current build next to it. Starting the GUI twice also covers and then start the current build next to it. Starting the GUI twice also covers
the `activate` guard: without it the second start adds a second window to the the `activate` guard: without it the second start adds a second window to the
running process. running process. The rows come along (see the takeover notes above), which is
checkable the same way: type into a row of the first instance without saving,
start the second one and read its rows back (next bullet) — they show the edit,
while the mappings file still has the old content. An instance from an older
commit cannot hand rows over, so there the new window shows the file.
The footer's daemon line is a plain `GtkLabel`, so its text can be read back over The footer's daemon line is a plain `GtkLabel`, so its text can be read back over
AT-SPI: `<app> → frame → panel → label` whose name starts with the translated AT-SPI: `<app> → frame → panel → label` whose name starts with the translated
@@ -433,7 +494,12 @@ is up. Three notes for tests:
`import -window $(wmctrl -l | grep 'gKeyMapper – Logitech' | awk '{print $1}') `import -window $(wmctrl -l | grep 'gKeyMapper – Logitech' | awk '{print $1}')
/tmp/gui.png`. The footer label works over AT-SPI (its accessible *name* is the /tmp/gui.png`. The footer label works over AT-SPI (its accessible *name* is the
text), but a screenshot of the cropped footer is easier and shows what the user text), but a screenshot of the cropped footer is easier and shows what the user
sees. sees. Easier than a screenshot for the rows themselves: the running instance
answers `gdbus call --session --dest dev.rager.gkeymapper --object-path
/dev/rager/gkeymapper/Window --method dev.rager.gkeymapper.Window.GetRows` with
exactly the mappings text its rows hold (`G1=command:/bin/true…`). It is the
same channel the takeover uses, so it also tells whether an edit is still
unsaved: compare it with the file.
- `XTest`-synthesized clicks need the window mapped and focused; the `Save` - `XTest`-synthesized clicks need the window mapped and focused; the `Save`
button is `push button` + name `Mappings speichern` in the German locale. button is `push button` + name `Mappings speichern` in the German locale.
@@ -447,12 +513,24 @@ The tray icon, the help and the options window need neither hardware nor a click
pid, two registrations) means the "register once" rule broke. pid, two registrations) means the "register once" rule broke.
- Its menu is readable without the panel: - Its menu is readable without the panel:
`gdbus call --session --dest org.kde.StatusNotifierItem-<pid>-1 --object-path `gdbus call --session --dest org.kde.StatusNotifierItem-<pid>-1 --object-path
/MenuBar --method com.canonical.dbusmenu.GetLayout 0 -1` returns the root node /MenuBar --method com.canonical.dbusmenu.GetLayout -- 0 -1 "[]"` returns the
with the two children. Drive a click the way the panel does — **batched**, as root node with the two children (the `--` keeps gdbus from reading the `-1` as
`EventGroup`, because that is the path a real click takes: an option, and the empty array is the property-name list `GetLayout` expects).
Drive a click the way the panel does — **batched**, as `EventGroup`, because
that is the path a real click takes:
`gdbus call … --method com.canonical.dbusmenu.EventGroup `gdbus call … --method com.canonical.dbusmenu.EventGroup
"[(1, 'clicked', <'', >, 0)]"` shows the window, id `2` quits the program. "[(1, 'clicked', <''>, 0)]"` shows the window, id `2` quits the program (the
A single `Event` call works too but is not what the watcher sends. empty string is the variant `v` of the event; `<''>` is the only spelling gdbus
accepts). A single `Event` call works too but is not what the watcher sends.
- A start with `--start-hidden` (what the autostart entry passes) is checkable the
same way: no window in `wmctrl -lp` while the item above is registered and the
process stays, and the menu's `Show` brings the window up. Without a tray watcher
the window has to appear by itself after ~5 s — on a private bus that is awkward
to see, because a fresh `dbus-daemon` makes GTK's startup wait on gvfs/portal
services (`GIO_USE_VFS=local` unblocks it, and it can still take ~20 s), so
`on_activate` runs much later than on the session bus. Do not read "no window" as
the flag working until the tray item or the window object
(`Window.GetRows`) proves the app got that far.
- The watcher is fragile by construction: it walks the layout it mirrors, so a - The watcher is fragile by construction: it walks the layout it mirrors, so a
malformed node (bare ids instead of full nodes) aborts it. After a change to malformed node (bare ids instead of full nodes) aborts it. After a change to
the layout, check it survived (`pgrep -a xapp-sn-watcher`) and that no new the layout, check it survived (`pgrep -a xapp-sn-watcher`) and that no new
@@ -482,6 +560,22 @@ The tray icon, the help and the options window need neither hardware nor a click
One pitfall while clicking: the options window is transient for the main One pitfall while clicking: the options window is transient for the main
window, so it always stays above it — clicks meant for the main window hit the window, so it always stays above it — clicks meant for the main window hit the
options window unless it is closed first. options window unless it is closed first.
- The popups (`src/popovers.cpp`) are checkable without hardware, and a click
test has to know two things: a popup is an override-redirect window that
disappears from `xwininfo`'s map state (or a `viewable.py`-style walk) but never
appears in `wmctrl -l`, and a click on the dropdown that is *already* open
closes it again — so "click the dropdown" does not mean "the popup is open",
always read the map state. What to check: with a type dropdown open, a click on
a *different* window must arrive there (`_NET_ACTIVE_WINDOW` changes to that
client, which proves the press was delivered and not swallowed) and the popup
must be gone; the same for a click on the bare desktop; a click in the window
outside the popup closes it without changing the focus; Escape closes it; a
click on an entry closes it and changes the row (`Window.GetRows`, e.g.
`G1=key:…` becomes `G1=command:…`); a click on the header bar's menu button
opens the menu and behaves like the dropdown. Every synthetic click must be
guarded by the id of the window that is expected to be topmost — the popup is
not in `_NET_CLIENT_LIST_STACKING`, so a click aimed at the popup has to be
checked against the window *below* it.
- The autostart switch needs no click to read and one click to write, because the - The autostart switch needs no click to read and one click to write, because the
`.desktop` entry *is* the state: put `$HOME/.config/autostart/gkeymapper.desktop` `.desktop` entry *is* the state: put `$HOME/.config/autostart/gkeymapper.desktop`
there (or delete it), close the options window (`wmctrl -i -c <id>`) and there (or delete it), close the options window (`wmctrl -i -c <id>`) and

View File

@@ -1,5 +1,6 @@
# gKeyMapper - Build # gKeyMapper - Build
# Dependencies: g++, pkg-config, msgfmt, libgtk-4-dev, libusb-1.0-0-dev, libxkbcommon-dev # Dependencies: g++, pkg-config, gettext (msgfmt, for the translations),
# libgtk-4-dev, libusb-1.0-0-dev, libxkbcommon-dev
# Installation: sudo apt install build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev # Installation: sudo apt install build-essential gettext libgtk-4-dev libusb-1.0-0-dev libxkbcommon-dev
CXX ?= g++ CXX ?= g++
@@ -42,12 +43,15 @@ DBUS_DIR := $(PREFIX)/share/dbus-1/services
CXXFLAGS += -DPACKAGE=\"$(PACKAGE)\" -DLOCALEDIR=\"$(LOCALEDIR)\" -DASSETS_DIR=\"$(ASSETS_DIR)\" CXXFLAGS += -DPACKAGE=\"$(PACKAGE)\" -DLOCALEDIR=\"$(LOCALEDIR)\" -DASSETS_DIR=\"$(ASSETS_DIR)\"
CXXFLAGS += -I$(SHARED_DIR) CXXFLAGS += -I$(SHARED_DIR)
# Translations # Translations. The .pot is generated from the sources (make pot) and the
# catalogs are merged against it (scripts/i18n.sh update); the rules are in
# po/README.md.
PO_FILES := $(wildcard po/*.po) PO_FILES := $(wildcard po/*.po)
LANGS := $(patsubst po/%.po,%,$(PO_FILES)) LANGS := $(patsubst po/%.po,%,$(PO_FILES))
MO_FILES := $(patsubst %,locale/%/LC_MESSAGES/$(PACKAGE).mo,$(LANGS)) MO_FILES := $(patsubst %,locale/%/LC_MESSAGES/$(PACKAGE).mo,$(LANGS))
I18N := $(CURDIR)/scripts/i18n.sh
.PHONY: all clean run install translations daemon .PHONY: all clean run install translations daemon pot i18n-check i18n-status
all: $(TARGET) translations $(DAEMON) all: $(TARGET) translations $(DAEMON)
@@ -72,6 +76,21 @@ locale/%/LC_MESSAGES/$(PACKAGE).mo: po/%.po
@mkdir -p $(@D) @mkdir -p $(@D)
$(MSGFMT) -o $@ $< $(MSGFMT) -o $@ $<
# Regenerate po/gkeymapper.pot from the sources. "make translations" does not: a
# pot that is written by a build is a pot that nobody has looked at, and the
# strings are reviewed by hand (po/README.md, "Adding or changing a string").
pot:
$(I18N) extract
# Everything that can be wrong with the translations at once: the pot against the
# sources, every catalog against the pot, and the language list of the GUI against
# po/*.po. Run it before committing a string change.
i18n-check:
$(I18N) check
i18n-status:
$(I18N) status
run: $(TARGET) translations run: $(TARGET) translations
GKEYMAPPER_LOCALEDIR=$(CURDIR)/locale GKEYMAPPER_ASSETS_DIR=$(CURDIR)/assets ./$(TARGET) GKEYMAPPER_LOCALEDIR=$(CURDIR)/locale GKEYMAPPER_ASSETS_DIR=$(CURDIR)/assets ./$(TARGET)

View File

@@ -75,6 +75,16 @@ window icon works without an installed `assets/` directory.
make translations make translations
``` ```
Adding or changing a translation, adding a language, and the checks around them
are described in [po/README.md](po/README.md). `scripts/i18n.sh` does the work and
is wired into the Makefile:
```bash
make pot # regenerate po/gkeymapper.pot from the sources
make i18n-check # pot, catalogs and the language list of the GUI
make i18n-status # per language: translated, fuzzy, untranslated
```
## Run ## Run
Normal: Normal:
@@ -105,8 +115,10 @@ window rewrites the same content instead of emptying the file.
Only one window is open at a time, and it is the one you just started: a new Only one window is open at a time, and it is the one you just started: a new
start closes the instance that is already running — including one from an older start closes the instance that is already running — including one from an older
build — and then shows its own window, so a stale window can never stay next to a build — and then shows its own window, so a stale window can never stay next to a
fresh one. Row edits that were not saved are lost with the replaced window, fresh one. The rows of the window it replaced come along, so an edit that was
because saving is always explicit (`Save mappings`). typed but never saved is still there; an instance from an older build cannot hand
them over, and then the window shows the saved mappings. Saving itself stays
explicit either way: press `Save mappings`.
### Menu, tray icon and minimize to tray ### Menu, tray icon and minimize to tray
@@ -122,12 +134,15 @@ configuration...`, `Options`, `Help` (also `F1`), `About` and `Quit` (`Ctrl+Q`).
(`System language`, or a fixed `de`/`en`/`es`/`fr`): it switches the window, the (`System language`, or a fixed `de`/`en`/`es`/`fr`): it switches the window, the
menus and the tray menu right away — nothing to restart, and rows that were not menus and the tray menu right away — nothing to restart, and rows that were not
saved yet are kept. saved yet are kept.
* `Start with the session` opens the window when you log in. It writes the usual * `Start with the session` starts the window minimized in the tray when you log
autostart entry (`~/.config/autostart/gkeymapper.desktop`, starting the binary in: it writes the usual autostart entry
that is running) and removes it again when you switch it off; an entry that the (`~/.config/autostart/gkeymapper.desktop`, starting the binary that is running
desktop's own startup tool has disabled counts as off, too. Like the window with the `--start-hidden` argument) and removes it again when you switch it off;
itself this is a convenience: the G keys do not need it, only the daemon has to an entry that the desktop's own startup tool has disabled counts as off, too. The
run (see `Running the daemon` below). window then waits in the tray until its `Show`, and only a desktop that hosts that
icon can leave it hidden — one without a tray gets the window a few seconds after
login. Like the window itself this is a convenience: the G keys do not need it,
only the daemon has to run (see `Running the daemon` below).
`Minimize to tray` keeps gKeyMapper reachable from the panel: the window is `Minimize to tray` keeps gKeyMapper reachable from the panel: the window is
hidden instead of closed when you close or minimize it, and the tray icon's menu hidden instead of closed when you close or minimize it, and the tray icon's menu
@@ -340,7 +355,8 @@ src/keyrow.h # KeyRow (one per G key)
src/mappings.{h,cpp} # GUI glue: config file <-> row widgets src/mappings.{h,cpp} # GUI glue: config file <-> row widgets
src/capturedialog.{h,cpp} # key-capture dialog src/capturedialog.{h,cpp} # key-capture dialog
src/daemonclient.{h,cpp} # D-Bus client for the daemon (status/reload) src/daemonclient.{h,cpp} # D-Bus client for the daemon (status/reload)
src/singleinstance.{h,cpp} # one GUI only: a new start closes a running instance src/singleinstance.{h,cpp} # one GUI only: a new start replaces a running one,
# taking the rows of its window along (unsaved edits)
src/trayicon.{h,cpp} # tray icon: StatusNotifierItem + dbusmenu over GIO src/trayicon.{h,cpp} # tray icon: StatusNotifierItem + dbusmenu over GIO
src/settings.{h,cpp} # gui.conf (window preferences, e.g. minimize to tray) src/settings.{h,cpp} # gui.conf (window preferences, e.g. minimize to tray)
src/help.{h,cpp} # help window src/help.{h,cpp} # help window
@@ -361,7 +377,8 @@ data/gkeymapper-uinput.conf # modules-load.d fragment for uinput
data/gkeymapper-daemon.service# systemd user unit (installed by make install) data/gkeymapper-daemon.service# systemd user unit (installed by make install)
data/dev.rager.gkeymapper.Daemon.service # D-Bus activation (session bus) data/dev.rager.gkeymapper.Daemon.service # D-Bus activation (session bus)
docs/daemon-plan.md # daemon plan (reference: g910-gkey-macro-support) docs/daemon-plan.md # daemon plan (reference: g910-gkey-macro-support)
po/ # Translations (pot/po/mo targets) po/ # Translations (pot/po/mo targets, po/README.md)
scripts/i18n.sh # extract/update/check/status/todo/new for the catalogs
Makefile # Build via pkg-config + gettext Makefile # Build via pkg-config + gettext
.vscode/tasks.json # VS Code tasks for make .vscode/tasks.json # VS Code tasks for make
README.md README.md

View File

@@ -361,6 +361,12 @@ Start-up orders:
comes from a build without the action — and `on_activate` re-presents the comes from a build without the action — and `on_activate` re-presents the
window that is already there instead of building a second one. A stale window window that is already there instead of building a second one. A stale window
therefore never stays next to a fresh one, and the daemon is not involved. therefore never stays next to a fresh one, and the daemon is not involved.
Closing the old window would drop row edits that were never saved, so the
takeover asks it for its rows first (`GetRows` at `/dev/rager/gkeymapper/Window`,
see `shared/ipc_ids.h`) and the new `build_ui()` starts from that mappings text
instead of from `mappings.conf`. An instance that does not answer — an older
build — leaves the new window with the file, and the file stays untouched until
the user presses `Save mappings`.
- Menu, help and options: `Help` (also `F1`) opens a one-page explanation of the - Menu, help and options: `Help` (also `F1`) opens a one-page explanation of the
rows, types, capture dialog, daemon status and tray; `Options` opens a small rows, types, capture dialog, daemon status and tray; `Options` opens a small
non-modal window with the GUI's own switches (tray behaviour, language, non-modal window with the GUI's own switches (tray behaviour, language,

178
po/README.md Normal file
View File

@@ -0,0 +1,178 @@
# Translations (i18n)
Everything about the catalogs, in one place: which file is which, how a string gets
from the source code into a translated window, and how to add a language. The
commands live in `scripts/i18n.sh`, wired into the Makefile as `pot`,
`i18n-check` and `i18n-status`.
## The pieces
| file | what it is |
| --- | --- |
| `po/gkeymapper.pot` | the template: every translatable string in the sources, with the file and line it comes from. **Generated** by `make pot` — never translated, and its msgids are never edited by hand. |
| `po/<lang>.po` | one catalog per language. This is what a translator edits. |
| `locale/<lang>/LC_MESSAGES/gkeymapper.mo` | the compiled catalog the program reads at runtime. Generated by `make translations`, gitignored. **Never edit** — it is overwritten on every build. |
| `scripts/i18n.sh` | `extract`, `update`, `check`, `status`, `todo`, `new` (see below). |
Languages: `de`, `en`, `es`, `fr`. The set is `po/*.po` — the Makefile picks up a
new catalog by itself (`LANGS`), nothing has to be registered.
`en.po` is special: English is the source language, and its catalog repeats every
msgid as its msgstr (an *identity* catalog). It exists so that choosing
"English" works on a German desktop, where the session locale would otherwise
select the German catalog. `i18n-check` enforces the identity — a translation that
drifts in is reported.
## How a string reaches the window
1. `_("...")` marks it in `src/` (or `shared/`) — the two directories the GUI is
built from. `N_("...")` is for strings that are only *collected* when they are
not translated yet: `src/help.cpp` keeps its paragraphs in a static array and
translates them when the window is built (`_(text)`). Both keywords are in the
pot; a string marked with neither is invisible to translators.
2. `make pot` writes `po/gkeymapper.pot` from the sources.
3. `scripts/i18n.sh update` merges the pot into every catalog. New strings come in
with an empty msgstr (= untranslated), existing translations are kept.
4. Translating is editing `po/<lang>.po`: replace the msgstr, leave the msgid
alone. Any text editor, or `poedit` if you prefer one.
5. `make translations` compiles `locale/<lang>/LC_MESSAGES/gkeymapper.mo`.
6. The program reads the catalog whose language it was started with. The GUI picks
it up automatically once gettext is set up in `src/main.cpp`; *which* language is
used comes from the options window (stored in `~/.config/gKeyMapper/gui.conf`,
`language=de`), or from the session when that setting is empty. Internally
`apply_gui_language()` (`src/settings.cpp`) sets `LANGUAGE` and re-runs
`setlocale` — `LANGUAGE` wins over `LANG`/`LC_ALL` in gettext, which is what
makes switching without a restart and an explicit `en` on a German desktop work.
The catalogs are searched under `$GKEYMAPPER_LOCALEDIR`, falling back to the
compiled-in `LOCALEDIR`.
## The common case: a string is added or changed
```bash
# 1. in the code
_("New text") # or N_("...") for a deferred one
# 2. the template
make pot # prints what is new and what is gone
# 3. what is missing where
make i18n-check # names every catalog that lacks it
# 4. every catalog takes it in (empty msgstr)
scripts/i18n.sh update # or: scripts/i18n.sh update de
# 5. translate
scripts/i18n.sh todo de # untranslated + fuzzy, with the source location
$EDITOR po/de.po # and es, fr - and en, where the msgstr is the msgid
# 6. check and compile
make i18n-check # must print "everything in sync"
make translations
# 7. see it
GKEYMAPPER_LOCALEDIR=$PWD/locale LANGUAGE=de_DE.UTF-8 ./gkeymapper
```
Commit the source change, `po/gkeymapper.pot` and the catalogs **together**: a pot
without the catalogs is a half-done change, and `make i18n-check` says so.
A new string in one place and the same text in another is *one* message for
gettext — the pot lists both locations, and one msgstr covers both.
## Adding a language
```bash
scripts/i18n.sh new it Italiano # po/it.po, header in the project's shape,
# every msgstr empty (the second word is
# Language-Team; it defaults to the code)
scripts/i18n.sh todo it # work through it
$EDITOR po/it.po
```
Then offer it in the GUI, or nobody can select it — `src/optionsdialog.cpp`:
* `kLanguageCodes[]` — the codes, in the same order as the labels ("", the empty
one, means "follow the session" and has to stay first);
* `languageOptions[]` — the labels. Use the **endonym** ("Italiano", not
"Italian"): the list reads the same in every language, so a user who lands in
the wrong language can still find their own.
`make i18n-check` checks exactly that pairing (a catalog that cannot be selected, a
code without a catalog, two arrays of different length). `make translations`
compiles the new catalog; `make run` and the test command above use the working
tree's `locale/`.
## Commands
| command | what it does |
| --- | --- |
| `scripts/i18n.sh extract` (`make pot`) | Regenerate `po/gkeymapper.pot` from the sources. Prints the messages that appeared and disappeared. Refuses to write an empty pot, so a broken gettext cannot empty the good one. |
| `scripts/i18n.sh update [lang...]` | Merge the pot into the catalogs, without source locations (see Conventions). Keeps translations, marks new strings untranslated. |
| `scripts/i18n.sh check` (`make i18n-check`) | Everything below. Exit code 1 when something is wrong, so it also works as a pre-commit step. |
| `scripts/i18n.sh status` (`make i18n-status`) | Per language: messages, translated, fuzzy, untranslated, and whether the compiled catalog is older than the po. |
| `scripts/i18n.sh todo <lang>` | The untranslated and fuzzy messages, with the source locations taken from the pot. The output is a valid po file, if you want to keep it. |
| `scripts/i18n.sh new <lang> [team]` | Create `po/<lang>.po` from the pot. `new en` writes the identity catalog. |
The gettext tools are taken from `PATH` and can be overridden like the Makefile's
variables, e.g. `XGETTEXT=/usr/bin/xgettext scripts/i18n.sh extract`.
## What `check` looks at
* the pot against the sources: same messages? A string added or changed without
`make pot` is the mistake this catches (a `#:` line number that moved is only
reported as a note — refresh it whenever it suits you);
* every catalog: it parses (`msgfmt -c`, which also compares the format
specifiers of msgid and msgstr), it defines the same messages as the pot, nothing
is untranslated or fuzzy, and its `Language:` header matches its file name;
* `po/en.po` is an identity catalog;
* `po/*.po` and the language list of the options window (`src/optionsdialog.cpp`)
agree;
* no strings are marked in `daemon/`: the daemon has no gettext setup, so `_()`
there would be translated into a catalog that is never read.
Notes that are *not* failures: catalogs that were not merged since the last
extract, and obsolete entries (`#~`, strings that are gone from the sources —
`msgattrib --no-obsolete po/<lang>.po > tmp && mv tmp po/<lang>.po` drops them).
## Conventions
* **No `#: file:line` comments in the catalogs.** The locations are noise for a
translator and churn whenever a line moves, so they live only in the pot
(`update` therefore uses `msgmerge --no-location`) and `todo` shows them from
there when they are needed. This is also why the catalogs are merged with the
script instead of a bare `msgmerge --update`, which would copy the locations in.
* **The build does not regenerate the pot** (`make translations` only runs
`msgfmt`). A template that is rewritten by every build is a template nobody
looks at; instead `make pot` is a deliberate step and `make i18n-check` fails
when it was forgotten.
* **Mnemonics stay mnemonics.** `_Cancel` uses the `_` to underline a letter;
`_Abbrechen` moves it to a letter that exists in that language. A translation
without the underscore loses the keyboard shortcut silently.
* **Format specifiers** (`%s`, `%d`) keep their number and order — `msgfmt -c`
catches a mismatch.
* **No plurals yet**: the program only uses `_()`, so no catalog carries a
`Plural-Forms` header. With the first `ngettext()` that header has to go into
every catalog (and `scripts/i18n.sh new` should write it).
* **HTML-ish markup** in a msgid (`<b>`, `<i>`) has to survive in the msgstr; the
help window renders it.
* German belongs in `po/de.po` only — sources, comments and documentation are
English.
## Troubleshooting
* *A window stays English while another window is translated* — the string is
missing from the catalog (or the catalog was not recompiled: `make translations`).
`scripts/i18n.sh todo <lang>` lists what is open.
* *Shortcut without an underline* — the mnemonic `_` is missing in the msgstr.
* *`po/*.po~` files appear* — that is `msgmerge`'s backup from a manual
`msgmerge --update`; the script deletes it. Never commit them.
* *A fuzzy entry is shown in English* — on purpose: fuzzy means "the text was
changed, this translation needs a look", and gettext falls back to the msgid
until it is confirmed (remove the `#, fuzzy` line).
* *"not merged against the current pot" in `check`* — run `scripts/i18n.sh update`;
until then the catalog header carries an older date than the pot.
* *Nothing is extracted at all* — `xgettext`'s built-in keyword list is not the
same everywhere, so the script always passes `--keyword=_ --keyword=N_`. If the
pot comes out empty, the script stops instead of replacing the pot.
The rest of the project's conventions (GUI, daemon, mappings) are in `AGENTS.md`.

317
po/de.po
View File

@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gkeymapper 0.1\n" "Project-Id-Version: gkeymapper 0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-18 03:08+0200\n" "POT-Creation-Date: 2026-10-18 20:18+0200\n"
"PO-Revision-Date: 2026-10-18 03:08+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
@@ -15,14 +15,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "Choose file" msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "Datei auswählen" msgstr "G-Key-Mapper für die Logitech G910 (GTK4)."
msgid "_Cancel"
msgstr "_Abbrechen"
msgid "_Open"
msgstr "_Öffnen"
msgid "No keys recorded yet" msgid "No keys recorded yet"
msgstr "Noch keine Tasten aufgezeichnet" msgstr "Noch keine Tasten aufgezeichnet"
@@ -30,6 +24,14 @@ msgstr "Noch keine Tasten aufgezeichnet"
msgid "Capture key sequence" msgid "Capture key sequence"
msgstr "Tastensequenz aufnehmen" msgstr "Tastensequenz aufnehmen"
msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgstr ""
"Tasten der Reihe nach drücken (Modifikatoren wie Strg, Alt, Shift, Super und "
"AltGr werden mit aufgezeichnet). Mit Fertig übernehmen oder mit Abbrechen "
"verwerfen."
msgid "Clear" msgid "Clear"
msgstr "Leeren" msgstr "Leeren"
@@ -40,12 +42,120 @@ msgid "Done"
msgstr "Fertig" msgstr "Fertig"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are " "<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
"recorded too). Click Done to apply or Cancel to discard." "the target and press <i>Save mappings</i>."
msgstr "" msgstr ""
"Tasten der Reihe nach drücken (Modifikatoren wie Strg, Alt, Shift, Super und " "<b>Die neun Zeilen</b> – eine pro G-Taste. Typ wählen, Ziel eingeben oder "
"AltGr werden mit aufgezeichnet). Mit Fertig übernehmen oder mit Abbrechen " "aufnehmen und <i>Mappings speichern</i> drücken."
"verwerfen."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c."
msgstr "<b>Taste</b> – drückt die aufgenommene Sequenz einmal, z. B. ctrl+c."
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, e."
"g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Taste (halten)</b> – drückt sie und hält sie, solange die G-Taste "
"gehalten wird, z. B. für Push-to-Talk. Nur ein einzelner Schritt kann "
"gehalten werden."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --"
"new-window."
msgstr ""
"<b>Befehl</b> – führt ein Programm oder eine Kommandozeile aus, z. B. /usr/"
"bin/firefox --new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Deaktiviert</b> – die Taste tut nichts."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then <i>Done</"
"i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Aufnehmen</b> – zeichnet die nächsten Tastenanschläge auf: drücken, dann "
"<i>Fertig</i>. Modifikatoren bleiben erhalten (shift, altgr, …) und die "
"Namen sind layoutunabhängig, ctrl+z bedeutet also die Taste, die auf deinem "
"Layout z tippt."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Mappings speichern</b> – schreibt ~/.config/gKeyMapper/mappings.conf und "
"lässt den Daemon sie neu laden. Die Menüeinträge <i>Konfiguration "
"exportieren...</i> und <i>Konfiguration importieren...</i> machen dasselbe "
"mit einer Datei deiner Wahl."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. <i>Options</"
"i> shows the same in more detail."
msgstr ""
"<b>Daemon-Status</b> – die Fußzeile zeigt, ob gkeymapper-daemon läuft, "
"welche Tastatur gefunden wurde und welche G-Taste zuletzt ausgelöst hat. "
"<i>Optionen</i> zeigt dasselbe ausführlicher."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> opens this window minimized "
"to the tray when you log in. The G keys work without it: gkeymapper-daemon "
"(a user service) reads them and replays the actions, and that daemon is the "
"only part that has to run."
msgstr ""
"<b>Autostart</b> – <i>Beim Anmelden starten</i> startet dieses Fenster "
"minimiert im Infobereich. Die G-Tasten funktionieren auch ohne: gkeymapper-"
"daemon (ein Benutzerdienst) liest sie und spielt die Aktionen ab – nur "
"dieser Daemon muss laufen."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved yet come "
"along."
msgstr ""
"<b>Infobereich und Fenster</b> – mit <i>In den Infobereich minimieren</i> "
"wird das Fenster beim Schließen oder Minimieren versteckt statt geschlossen; "
"das Symbol im Infobereich zeigt es wieder (<i>Anzeigen</i>) und beendet das "
"Programm (<i>Beenden</i>). Ein erneuter Start von gKeyMapper schließt das "
"alte Fenster – noch nicht gespeicherte Zeilen kommen dabei mit."
msgid "Help"
msgstr "Hilfe"
msgid "Close window"
msgstr "Fenster schließen"
msgid "Daemon: not reachable"
msgstr "Daemon: nicht erreichbar"
msgid "Daemon: running"
msgstr "Daemon: läuft"
msgid "Device: not found"
msgstr "Gerät: nicht gefunden"
#, c-format
msgid "Device: %s"
msgstr "Gerät: %s"
msgid "Configuration error"
msgstr "Konfigurationsfehler"
#, c-format
msgid "Last key: %s"
msgstr "Letzte Taste: %s"
msgid "Choose file"
msgstr "Datei auswählen"
msgid "_Cancel"
msgstr "_Abbrechen"
msgid "_Open"
msgstr "_Öffnen"
msgid "Choose file..." msgid "Choose file..."
msgstr "Datei auswählen..." msgstr "Datei auswählen..."
@@ -56,12 +166,12 @@ msgstr "z. B. /usr/bin/firefox oder eigener Befehl"
msgid "Capture" msgid "Capture"
msgstr "Aufnehmen" msgstr "Aufnehmen"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "z. B. Strg+C, Strg+V"
msgid "e.g. ctrl+w (held while the G key is held)" msgid "e.g. ctrl+w (held while the G key is held)"
msgstr "z. B. Strg+W (gehalten, solange die G-Taste gedrückt ist)" msgstr "z. B. Strg+W (gehalten, solange die G-Taste gedrückt ist)"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "z. B. Strg+C, Strg+V"
#, c-format #, c-format
msgid "Could not save %s.\n" msgid "Could not save %s.\n"
msgstr "Konnte %s nicht speichern.\n" msgstr "Konnte %s nicht speichern.\n"
@@ -69,19 +179,12 @@ msgstr "Konnte %s nicht speichern.\n"
msgid "Mappings saved to " msgid "Mappings saved to "
msgstr "Mappings gespeichert nach " msgstr "Mappings gespeichert nach "
#, c-format
msgid "Could not open %s.\n"
msgstr "Konnte %s nicht öffnen.\n"
msgid "Could not export mappings.\n" msgid "Could not export mappings.\n"
msgstr "Mappings konnten nicht exportiert werden.\n" msgstr "Mappings konnten nicht exportiert werden.\n"
msgid "gKeyMapper" msgid "gKeyMapper"
msgstr "gKeyMapper" msgstr "gKeyMapper"
msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "G-Key-Mapper für die Logitech G910 (GTK4)."
msgid "Export configuration" msgid "Export configuration"
msgstr "Konfiguration exportieren" msgstr "Konfiguration exportieren"
@@ -100,9 +203,15 @@ msgstr "Konfiguration exportieren..."
msgid "Import configuration..." msgid "Import configuration..."
msgstr "Konfiguration importieren..." msgstr "Konfiguration importieren..."
msgid "Options"
msgstr "Optionen"
msgid "About" msgid "About"
msgstr "Über" msgstr "Über"
msgid "Quit"
msgstr "Beenden"
msgid "G1 to G9 of the Logitech G910 remapped to keys or commands" msgid "G1 to G9 of the Logitech G910 remapped to keys or commands"
msgstr "G1 bis G9 der Logitech G910 auf Tasten oder Befehle ummappen" msgstr "G1 bis G9 der Logitech G910 auf Tasten oder Befehle ummappen"
@@ -136,44 +245,12 @@ msgstr "Mappings speichern"
msgid "Saved to ~/.config/gKeyMapper/mappings.conf" msgid "Saved to ~/.config/gKeyMapper/mappings.conf"
msgstr "Gespeichert in ~/.config/gKeyMapper/mappings.conf" msgstr "Gespeichert in ~/.config/gKeyMapper/mappings.conf"
msgid "Daemon: not reachable"
msgstr "Daemon: nicht erreichbar"
msgid "Daemon: running"
msgstr "Daemon: läuft"
msgid "Device: %s"
msgstr "Gerät: %s"
msgid "Device: not found"
msgstr "Gerät: nicht gefunden"
msgid "Configuration error"
msgstr "Konfigurationsfehler"
msgid "Last key: %s"
msgstr "Letzte Taste: %s"
msgid "Daemon: checking..." msgid "Daemon: checking..."
msgstr "Daemon: wird geprüft ..." msgstr "Daemon: wird geprüft ..."
msgid "Options" #, c-format
msgstr "Optionen" msgid "Could not open %s.\n"
msgstr "Konnte %s nicht öffnen.\n"
msgid "Help"
msgstr "Hilfe"
msgid "Quit"
msgstr "Beenden"
msgid "Show"
msgstr "Anzeigen"
msgid "Close"
msgstr "Beenden"
msgid "Close window"
msgstr "Fenster schließen"
msgid "Window" msgid "Window"
msgstr "Fenster" msgstr "Fenster"
@@ -181,13 +258,15 @@ msgstr "Fenster"
msgid "Minimize to tray" msgid "Minimize to tray"
msgstr "In den Infobereich minimieren" msgstr "In den Infobereich minimieren"
msgid "Start with the session"
msgstr "Beim Anmelden starten"
msgid "" msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon " "Opens this window minimized to the tray when you log in. Not needed for the "
"shows it again (Show) and quits the program (Close)." "G keys - the daemon does the work and is the only part that has to run."
msgstr "" msgstr ""
"Beim Schließen oder Minimieren wird das Fenster versteckt statt beendet. Das " "Öffnet dieses Fenster beim Anmelden minimiert im Infobereich. Für die G-"
"Symbol im Infobereich zeigt es wieder (Anzeigen) und beendet das Programm " "Tasten nicht nötig – die Arbeit macht der Daemon, und nur der muss laufen."
"(Beenden)."
msgid "Language" msgid "Language"
msgstr "Sprache" msgstr "Sprache"
@@ -198,21 +277,6 @@ msgstr "Systemsprache"
msgid "The interface switches right away, including the tray menu." msgid "The interface switches right away, including the tray menu."
msgstr "Die Oberfläche wechselt sofort – auch das Menü im Infobereich." msgstr "Die Oberfläche wechselt sofort – auch das Menü im Infobereich."
msgid "Start with the session"
msgstr "Beim Anmelden starten"
msgid ""
"Opens this window when you log in. Not needed for the G keys - the daemon "
"does the work and is the only part that has to run."
msgstr ""
"Öffnet dieses Fenster beim Anmelden. Für die G-Tasten nicht nötig – die "
"Arbeit macht der Daemon, und nur der muss laufen."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
"Auf diesem Desktop gibt es keinen Infobereich, daher kann das Fenster nicht "
"versteckt werden."
msgid "Daemon status" msgid "Daemon status"
msgstr "Daemon-Status" msgstr "Daemon-Status"
@@ -234,6 +298,19 @@ msgstr "Konfigurationsdatei"
msgid "Last key" msgid "Last key"
msgstr "Letzte Taste" msgstr "Letzte Taste"
msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgstr ""
"Beim Schließen oder Minimieren wird das Fenster versteckt statt beendet. Das "
"Symbol im Infobereich zeigt es wieder (Anzeigen) und beendet das Programm "
"(Beenden)."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
"Auf diesem Desktop gibt es keinen Infobereich, daher kann das Fenster nicht "
"versteckt werden."
msgid "active" msgid "active"
msgstr "aktiv" msgstr "aktiv"
@@ -249,82 +326,8 @@ msgstr "nicht erreichbar"
msgid "not found" msgid "not found"
msgstr "nicht gefunden" msgstr "nicht gefunden"
msgid "" msgid "Show"
"<b>The nine rows</b> – one per G key. Choose the type, then enter or capture " msgstr "Anzeigen"
"the target and press <i>Save mappings</i>."
msgstr ""
"<b>Die neun Zeilen</b> – eine pro G-Taste. Typ wählen, Ziel eingeben oder "
"aufnehmen und <i>Mappings speichern</i> drücken."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c." msgid "Close"
msgstr "<b>Taste</b> – drückt die aufgenommene Sequenz einmal, z. B. ctrl+c." msgstr "Beenden"
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, "
"e.g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Taste (halten)</b> – drückt sie und hält sie, solange die G-Taste "
"gehalten wird, z. B. für Push-to-Talk. Nur ein einzelner Schritt kann "
"gehalten werden."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox "
"--new-window."
msgstr ""
"<b>Befehl</b> – führt ein Programm oder eine Kommandozeile aus, z. B. "
"/usr/bin/firefox --new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Deaktiviert</b> – die Taste tut nichts."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then "
"<i>Done</i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Aufnehmen</b> – zeichnet die nächsten Tastenanschläge auf: drücken, dann "
"<i>Fertig</i>. Modifikatoren bleiben erhalten (shift, altgr, …) und die "
"Namen sind layoutunabhängig, ctrl+z bedeutet also die Taste, die auf deinem "
"Layout z tippt."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Mappings speichern</b> – schreibt ~/.config/gKeyMapper/mappings.conf und "
"lässt den Daemon sie neu laden. Die Menüeinträge <i>Konfiguration "
"exportieren...</i> und <i>Konfiguration importieren...</i> machen dasselbe "
"mit einer Datei deiner Wahl."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. "
"<i>Options</i> shows the same in more detail."
msgstr ""
"<b>Daemon-Status</b> – die Fußzeile zeigt, ob gkeymapper-daemon läuft, "
"welche Tastatur gefunden wurde und welche G-Taste zuletzt ausgelöst hat. "
"<i>Optionen</i> zeigt dasselbe ausführlicher."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> only opens this window when "
"you log in. The G keys work without it: gkeymapper-daemon (a user service) "
"reads them and replays the actions, and that daemon is the only part that has "
"to run."
msgstr ""
"<b>Autostart</b> – <i>Beim Anmelden starten</i> öffnet nur dieses Fenster. "
"Die G-Tasten funktionieren auch ohne: gkeymapper-daemon (ein Benutzerdienst) "
"liest sie und spielt die Aktionen ab – nur dieser Daemon muss laufen."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved are lost "
"with it."
msgstr ""
"<b>Infobereich und Fenster</b> – mit <i>In den Infobereich minimieren</i> "
"wird das Fenster beim Schließen oder Minimieren versteckt statt geschlossen; "
"das Symbol im Infobereich zeigt es wieder (<i>Anzeigen</i>) und beendet das "
"Programm (<i>Beenden</i>). Ein erneuter Start von gKeyMapper schließt das "
"alte Fenster – nicht gespeicherte Zeilen gehen damit verloren."

304
po/en.po
View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gkeymapper 0.1\n" "Project-Id-Version: gkeymapper 0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-18 03:08+0200\n" "POT-Creation-Date: 2026-10-18 20:18+0200\n"
"PO-Revision-Date: 2026-10-18 03:08+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -16,14 +16,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "Choose file" msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "Choose file" msgstr "G-Key mapper for the Logitech G910 (GTK4)."
msgid "_Cancel"
msgstr "_Cancel"
msgid "_Open"
msgstr "_Open"
msgid "No keys recorded yet" msgid "No keys recorded yet"
msgstr "No keys recorded yet" msgstr "No keys recorded yet"
@@ -31,6 +25,13 @@ msgstr "No keys recorded yet"
msgid "Capture key sequence" msgid "Capture key sequence"
msgstr "Capture key sequence" msgstr "Capture key sequence"
msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgstr ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgid "Clear" msgid "Clear"
msgstr "Clear" msgstr "Clear"
@@ -41,11 +42,117 @@ msgid "Done"
msgstr "Done" msgstr "Done"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are " "<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
"recorded too). Click Done to apply or Cancel to discard." "the target and press <i>Save mappings</i>."
msgstr "" msgstr ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are " "<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
"recorded too). Click Done to apply or Cancel to discard." "the target and press <i>Save mappings</i>."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c."
msgstr "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c."
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, e."
"g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, e."
"g. for push-to-talk. Only a single step can be held."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --"
"new-window."
msgstr ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --"
"new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Disabled</b> – the key does nothing."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then <i>Done</"
"i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Capture</b> – records the keys you press next: press them, then <i>Done</"
"i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. <i>Options</"
"i> shows the same in more detail."
msgstr ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. <i>Options</"
"i> shows the same in more detail."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> opens this window minimized "
"to the tray when you log in. The G keys work without it: gkeymapper-daemon "
"(a user service) reads them and replays the actions, and that daemon is the "
"only part that has to run."
msgstr ""
"<b>Autostart</b> – <i>Start with the session</i> opens this window minimized "
"to the tray when you log in. The G keys work without it: gkeymapper-daemon "
"(a user service) reads them and replays the actions, and that daemon is the "
"only part that has to run."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved yet come "
"along."
msgstr ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved yet come "
"along."
msgid "Help"
msgstr "Help"
msgid "Close window"
msgstr "Close window"
msgid "Daemon: not reachable"
msgstr "Daemon: not reachable"
msgid "Daemon: running"
msgstr "Daemon: running"
msgid "Device: not found"
msgstr "Device: not found"
#, c-format
msgid "Device: %s"
msgstr "Device: %s"
msgid "Configuration error"
msgstr "Configuration error"
#, c-format
msgid "Last key: %s"
msgstr "Last key: %s"
msgid "Choose file"
msgstr "Choose file"
msgid "_Cancel"
msgstr "_Cancel"
msgid "_Open"
msgstr "_Open"
msgid "Choose file..." msgid "Choose file..."
msgstr "Choose file..." msgstr "Choose file..."
@@ -56,12 +163,12 @@ msgstr "e.g. /usr/bin/firefox or custom command"
msgid "Capture" msgid "Capture"
msgstr "Capture" msgstr "Capture"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "e.g. ctrl+c, ctrl+v"
msgid "e.g. ctrl+w (held while the G key is held)" msgid "e.g. ctrl+w (held while the G key is held)"
msgstr "e.g. ctrl+w (held while the G key is held)" msgstr "e.g. ctrl+w (held while the G key is held)"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "e.g. ctrl+c, ctrl+v"
#, c-format #, c-format
msgid "Could not save %s.\n" msgid "Could not save %s.\n"
msgstr "Could not save %s.\n" msgstr "Could not save %s.\n"
@@ -69,19 +176,12 @@ msgstr "Could not save %s.\n"
msgid "Mappings saved to " msgid "Mappings saved to "
msgstr "Mappings saved to " msgstr "Mappings saved to "
#, c-format
msgid "Could not open %s.\n"
msgstr "Could not open %s.\n"
msgid "Could not export mappings.\n" msgid "Could not export mappings.\n"
msgstr "Could not export mappings.\n" msgstr "Could not export mappings.\n"
msgid "gKeyMapper" msgid "gKeyMapper"
msgstr "gKeyMapper" msgstr "gKeyMapper"
msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "G-Key mapper for the Logitech G910 (GTK4)."
msgid "Export configuration" msgid "Export configuration"
msgstr "Export configuration" msgstr "Export configuration"
@@ -100,9 +200,15 @@ msgstr "Export configuration..."
msgid "Import configuration..." msgid "Import configuration..."
msgstr "Import configuration..." msgstr "Import configuration..."
msgid "Options"
msgstr "Options"
msgid "About" msgid "About"
msgstr "About" msgstr "About"
msgid "Quit"
msgstr "Quit"
msgid "G1 to G9 of the Logitech G910 remapped to keys or commands" msgid "G1 to G9 of the Logitech G910 remapped to keys or commands"
msgstr "G1 to G9 of the Logitech G910 remapped to keys or commands" msgstr "G1 to G9 of the Logitech G910 remapped to keys or commands"
@@ -136,44 +242,12 @@ msgstr "Save mappings"
msgid "Saved to ~/.config/gKeyMapper/mappings.conf" msgid "Saved to ~/.config/gKeyMapper/mappings.conf"
msgstr "Saved to ~/.config/gKeyMapper/mappings.conf" msgstr "Saved to ~/.config/gKeyMapper/mappings.conf"
msgid "Daemon: not reachable"
msgstr "Daemon: not reachable"
msgid "Daemon: running"
msgstr "Daemon: running"
msgid "Device: %s"
msgstr "Device: %s"
msgid "Device: not found"
msgstr "Device: not found"
msgid "Configuration error"
msgstr "Configuration error"
msgid "Last key: %s"
msgstr "Last key: %s"
msgid "Daemon: checking..." msgid "Daemon: checking..."
msgstr "Daemon: checking..." msgstr "Daemon: checking..."
msgid "Options" #, c-format
msgstr "Options" msgid "Could not open %s.\n"
msgstr "Could not open %s.\n"
msgid "Help"
msgstr "Help"
msgid "Quit"
msgstr "Quit"
msgid "Show"
msgstr "Show"
msgid "Close"
msgstr "Close"
msgid "Close window"
msgstr "Close window"
msgid "Window" msgid "Window"
msgstr "Window" msgstr "Window"
@@ -181,12 +255,15 @@ msgstr "Window"
msgid "Minimize to tray" msgid "Minimize to tray"
msgstr "Minimize to tray" msgstr "Minimize to tray"
msgid "Start with the session"
msgstr "Start with the session"
msgid "" msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon " "Opens this window minimized to the tray when you log in. Not needed for the "
"shows it again (Show) and quits the program (Close)." "G keys - the daemon does the work and is the only part that has to run."
msgstr "" msgstr ""
"Closing or minimizing the window hides it instead of quitting. The tray icon " "Opens this window minimized to the tray when you log in. Not needed for the "
"shows it again (Show) and quits the program (Close)." "G keys - the daemon does the work and is the only part that has to run."
msgid "Language" msgid "Language"
msgstr "Language" msgstr "Language"
@@ -197,19 +274,6 @@ msgstr "System language"
msgid "The interface switches right away, including the tray menu." msgid "The interface switches right away, including the tray menu."
msgstr "The interface switches right away, including the tray menu." msgstr "The interface switches right away, including the tray menu."
msgid "Start with the session"
msgstr "Start with the session"
msgid ""
"Opens this window when you log in. Not needed for the G keys - the daemon "
"does the work and is the only part that has to run."
msgstr ""
"Opens this window when you log in. Not needed for the G keys - the daemon "
"does the work and is the only part that has to run."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr "No tray is available on this desktop, so the window cannot be hidden."
msgid "Daemon status" msgid "Daemon status"
msgstr "Daemon status" msgstr "Daemon status"
@@ -231,6 +295,16 @@ msgstr "Configuration file"
msgid "Last key" msgid "Last key"
msgstr "Last key" msgstr "Last key"
msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgstr ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr "No tray is available on this desktop, so the window cannot be hidden."
msgid "active" msgid "active"
msgstr "active" msgstr "active"
@@ -246,80 +320,8 @@ msgstr "not reachable"
msgid "not found" msgid "not found"
msgstr "not found" msgstr "not found"
msgid "" msgid "Show"
"<b>The nine rows</b> – one per G key. Choose the type, then enter or capture " msgstr "Show"
"the target and press <i>Save mappings</i>."
msgstr ""
"<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
"the target and press <i>Save mappings</i>."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c." msgid "Close"
msgstr "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c." msgstr "Close"
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, "
"e.g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, "
"e.g. for push-to-talk. Only a single step can be held."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox "
"--new-window."
msgstr ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox "
"--new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Disabled</b> – the key does nothing."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then "
"<i>Done</i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Capture</b> – records the keys you press next: press them, then "
"<i>Done</i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. "
"<i>Options</i> shows the same in more detail."
msgstr ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. "
"<i>Options</i> shows the same in more detail."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> only opens this window when "
"you log in. The G keys work without it: gkeymapper-daemon (a user service) "
"reads them and replays the actions, and that daemon is the only part that has "
"to run."
msgstr ""
"<b>Autostart</b> – <i>Start with the session</i> only opens this window when "
"you log in. The G keys work without it: gkeymapper-daemon (a user service) "
"reads them and replays the actions, and that daemon is the only part that has "
"to run."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved are lost "
"with it."
msgstr ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved are lost "
"with it."

313
po/es.po
View File

@@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gkeymapper 0.1\n" "Project-Id-Version: gkeymapper 0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-18 03:08+0200\n" "POT-Creation-Date: 2026-10-18 20:18+0200\n"
"PO-Revision-Date: 2026-10-18 03:08+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
@@ -13,14 +13,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "Choose file" msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "Elegir archivo" msgstr "Asignador de teclas G para el Logitech G910 (GTK4)."
msgid "_Cancel"
msgstr "_Cancelar"
msgid "_Open"
msgstr "_Abrir"
msgid "No keys recorded yet" msgid "No keys recorded yet"
msgstr "Aún no se ha registrado ninguna tecla" msgstr "Aún no se ha registrado ninguna tecla"
@@ -28,6 +22,14 @@ msgstr "Aún no se ha registrado ninguna tecla"
msgid "Capture key sequence" msgid "Capture key sequence"
msgstr "Capturar secuencia de teclas" msgstr "Capturar secuencia de teclas"
msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgstr ""
"Pulsa las teclas en orden (los modificadores como Ctrl, Alt, Mayús, Super y "
"AltGr también se registran). Haz clic en Aceptar para aplicar o en Cancelar "
"para descartar."
msgid "Clear" msgid "Clear"
msgstr "Borrar" msgstr "Borrar"
@@ -38,12 +40,119 @@ msgid "Done"
msgstr "Aceptar" msgstr "Aceptar"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are " "<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
"recorded too). Click Done to apply or Cancel to discard." "the target and press <i>Save mappings</i>."
msgstr "" msgstr ""
"Pulsa las teclas en orden (los modificadores como Ctrl, Alt, Mayús, Super y " "<b>Las nueve filas</b> – una por tecla G. Elige el tipo, introduce o captura "
"AltGr también se registran). Haz clic en Aceptar para aplicar o en Cancelar " "el destino y pulsa <i>Guardar asignaciones</i>."
"para descartar."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c."
msgstr "<b>Tecla</b> – pulsa la secuencia capturada una vez, p. ej. ctrl+c."
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, e."
"g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Tecla (mantener)</b> – la pulsa y la mantiene mientras se mantiene la "
"tecla G, p. ej. para pulsar para hablar. Solo se puede mantener un paso."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --"
"new-window."
msgstr ""
"<b>Comando</b> – ejecuta un programa o una línea de comandos, p. ej. /usr/"
"bin/firefox --new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Desactivado</b> – la tecla no hace nada."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then <i>Done</"
"i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Capturar</b> – graba las siguientes teclas que pulses: púlsalas y luego "
"<i>Aceptar</i>. Los modificadores se conservan (shift, altgr, …) y los "
"nombres no dependen de la distribución, así que ctrl+z es la tecla que "
"escribe z en tu distribución."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Guardar asignaciones</b> – escribe ~/.config/gKeyMapper/mappings.conf y "
"hace que el daemon la recargue. Las entradas de menú <i>Exportar "
"configuración...</i> e <i>Importar configuración...</i> hacen lo mismo con "
"el archivo que elijas."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. <i>Options</"
"i> shows the same in more detail."
msgstr ""
"<b>Estado del daemon</b> – el pie de ventana muestra si gkeymapper-daemon "
"está en ejecución, qué teclado ha encontrado y la última tecla G que se ha "
"pulsado. <i>Opciones</i> muestra lo mismo con más detalle."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> opens this window minimized "
"to the tray when you log in. The G keys work without it: gkeymapper-daemon "
"(a user service) reads them and replays the actions, and that daemon is the "
"only part that has to run."
msgstr ""
"<b>Inicio automático</b> – <i>Iniciar con la sesión</i> abre esta ventana "
"minimizada en la bandeja al iniciar la sesión. Las teclas G funcionan sin "
"ella: gkeymapper-daemon (un servicio de usuario) las lee y reproduce las "
"acciones, y ese daemon es lo único que tiene que estar en marcha."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved yet come "
"along."
msgstr ""
"<b>Bandeja y ventana</b> – con <i>Minimizar a la bandeja</i>, al cerrar o "
"minimizar la ventana se oculta en lugar de cerrarse; el icono de la bandeja "
"la vuelve a mostrar (<i>Mostrar</i>) y cierra el programa (<i>Cerrar</i>). "
"Volver a iniciar gKeyMapper cierra la ventana antigua: las filas aún sin "
"guardar se conservan."
msgid "Help"
msgstr "Ayuda"
msgid "Close window"
msgstr "Cerrar ventana"
msgid "Daemon: not reachable"
msgstr "Demonio: no accesible"
msgid "Daemon: running"
msgstr "Demonio: en ejecución"
msgid "Device: not found"
msgstr "Dispositivo: no encontrado"
#, c-format
msgid "Device: %s"
msgstr "Dispositivo: %s"
msgid "Configuration error"
msgstr "Error de configuración"
#, c-format
msgid "Last key: %s"
msgstr "Última tecla: %s"
msgid "Choose file"
msgstr "Elegir archivo"
msgid "_Cancel"
msgstr "_Cancelar"
msgid "_Open"
msgstr "_Abrir"
msgid "Choose file..." msgid "Choose file..."
msgstr "Elegir archivo..." msgstr "Elegir archivo..."
@@ -54,12 +163,12 @@ msgstr "p. ej. /usr/bin/firefox o comando personalizado"
msgid "Capture" msgid "Capture"
msgstr "Capturar" msgstr "Capturar"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "p. ej. ctrl+c, ctrl+v"
msgid "e.g. ctrl+w (held while the G key is held)" msgid "e.g. ctrl+w (held while the G key is held)"
msgstr "p. ej. ctrl+w (mantenida mientras se mantiene pulsada la tecla G)" msgstr "p. ej. ctrl+w (mantenida mientras se mantiene pulsada la tecla G)"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "p. ej. ctrl+c, ctrl+v"
#, c-format #, c-format
msgid "Could not save %s.\n" msgid "Could not save %s.\n"
msgstr "No se pudo guardar %s.\n" msgstr "No se pudo guardar %s.\n"
@@ -67,19 +176,12 @@ msgstr "No se pudo guardar %s.\n"
msgid "Mappings saved to " msgid "Mappings saved to "
msgstr "Asignaciones guardadas en " msgstr "Asignaciones guardadas en "
#, c-format
msgid "Could not open %s.\n"
msgstr "No se pudo abrir %s.\n"
msgid "Could not export mappings.\n" msgid "Could not export mappings.\n"
msgstr "No se pudieron exportar las asignaciones.\n" msgstr "No se pudieron exportar las asignaciones.\n"
msgid "gKeyMapper" msgid "gKeyMapper"
msgstr "gKeyMapper" msgstr "gKeyMapper"
msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "Asignador de teclas G para el Logitech G910 (GTK4)."
msgid "Export configuration" msgid "Export configuration"
msgstr "Exportar configuración" msgstr "Exportar configuración"
@@ -98,9 +200,15 @@ msgstr "Exportar configuración..."
msgid "Import configuration..." msgid "Import configuration..."
msgstr "Importar configuración..." msgstr "Importar configuración..."
msgid "Options"
msgstr "Opciones"
msgid "About" msgid "About"
msgstr "Acerca de" msgstr "Acerca de"
msgid "Quit"
msgstr "Salir"
msgid "G1 to G9 of the Logitech G910 remapped to keys or commands" msgid "G1 to G9 of the Logitech G910 remapped to keys or commands"
msgstr "Reasigna las G1 a G9 del Logitech G910 a teclas o comandos" msgstr "Reasigna las G1 a G9 del Logitech G910 a teclas o comandos"
@@ -134,44 +242,12 @@ msgstr "Guardar asignaciones"
msgid "Saved to ~/.config/gKeyMapper/mappings.conf" msgid "Saved to ~/.config/gKeyMapper/mappings.conf"
msgstr "Guardado en ~/.config/gKeyMapper/mappings.conf" msgstr "Guardado en ~/.config/gKeyMapper/mappings.conf"
msgid "Daemon: not reachable"
msgstr "Demonio: no accesible"
msgid "Daemon: running"
msgstr "Demonio: en ejecución"
msgid "Device: %s"
msgstr "Dispositivo: %s"
msgid "Device: not found"
msgstr "Dispositivo: no encontrado"
msgid "Configuration error"
msgstr "Error de configuración"
msgid "Last key: %s"
msgstr "Última tecla: %s"
msgid "Daemon: checking..." msgid "Daemon: checking..."
msgstr "Demonio: comprobando..." msgstr "Demonio: comprobando..."
msgid "Options" #, c-format
msgstr "Opciones" msgid "Could not open %s.\n"
msgstr "No se pudo abrir %s.\n"
msgid "Help"
msgstr "Ayuda"
msgid "Quit"
msgstr "Salir"
msgid "Show"
msgstr "Mostrar"
msgid "Close"
msgstr "Cerrar"
msgid "Close window"
msgstr "Cerrar ventana"
msgid "Window" msgid "Window"
msgstr "Ventana" msgstr "Ventana"
@@ -179,12 +255,16 @@ msgstr "Ventana"
msgid "Minimize to tray" msgid "Minimize to tray"
msgstr "Minimizar a la bandeja" msgstr "Minimizar a la bandeja"
msgid "Start with the session"
msgstr "Iniciar con la sesión"
msgid "" msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon " "Opens this window minimized to the tray when you log in. Not needed for the "
"shows it again (Show) and quits the program (Close)." "G keys - the daemon does the work and is the only part that has to run."
msgstr "" msgstr ""
"Al cerrar o minimizar, la ventana se oculta en lugar de salir. El icono de " "Abre esta ventana minimizada en la bandeja al iniciar la sesión. No hace "
"la bandeja la vuelve a mostrar (Mostrar) y cierra el programa (Cerrar)." "falta para las teclas G: el trabajo lo hace el daemon, y solo él tiene que "
"estar en marcha."
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
@@ -195,20 +275,6 @@ msgstr "Idioma del sistema"
msgid "The interface switches right away, including the tray menu." msgid "The interface switches right away, including the tray menu."
msgstr "La interfaz cambia al instante, incluido el menú de la bandeja." msgstr "La interfaz cambia al instante, incluido el menú de la bandeja."
msgid "Start with the session"
msgstr "Iniciar con la sesión"
msgid ""
"Opens this window when you log in. Not needed for the G keys - the daemon "
"does the work and is the only part that has to run."
msgstr ""
"Abre esta ventana al iniciar la sesión. No hace falta para las teclas G: el "
"trabajo lo hace el daemon, y solo él tiene que estar en marcha."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
"Este escritorio no tiene bandeja, así que la ventana no se puede ocultar."
msgid "Daemon status" msgid "Daemon status"
msgstr "Estado del daemon" msgstr "Estado del daemon"
@@ -230,6 +296,17 @@ msgstr "Archivo de configuración"
msgid "Last key" msgid "Last key"
msgstr "Última tecla" msgstr "Última tecla"
msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgstr ""
"Al cerrar o minimizar, la ventana se oculta en lugar de salir. El icono de "
"la bandeja la vuelve a mostrar (Mostrar) y cierra el programa (Cerrar)."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
"Este escritorio no tiene bandeja, así que la ventana no se puede ocultar."
msgid "active" msgid "active"
msgstr "activo" msgstr "activo"
@@ -245,82 +322,8 @@ msgstr "no accesible"
msgid "not found" msgid "not found"
msgstr "no encontrado" msgstr "no encontrado"
msgid "" msgid "Show"
"<b>The nine rows</b> – one per G key. Choose the type, then enter or capture " msgstr "Mostrar"
"the target and press <i>Save mappings</i>."
msgstr ""
"<b>Las nueve filas</b> – una por tecla G. Elige el tipo, introduce o captura "
"el destino y pulsa <i>Guardar asignaciones</i>."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c." msgid "Close"
msgstr "<b>Tecla</b> – pulsa la secuencia capturada una vez, p. ej. ctrl+c." msgstr "Cerrar"
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, "
"e.g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Tecla (mantener)</b> – la pulsa y la mantiene mientras se mantiene la "
"tecla G, p. ej. para pulsar para hablar. Solo se puede mantener un paso."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox "
"--new-window."
msgstr ""
"<b>Comando</b> – ejecuta un programa o una línea de comandos, p. ej. "
"/usr/bin/firefox --new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Desactivado</b> – la tecla no hace nada."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then "
"<i>Done</i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Capturar</b> – graba las siguientes teclas que pulses: púlsalas y luego "
"<i>Aceptar</i>. Los modificadores se conservan (shift, altgr, …) y los "
"nombres no dependen de la distribución, así que ctrl+z es la tecla que "
"escribe z en tu distribución."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Guardar asignaciones</b> – escribe ~/.config/gKeyMapper/mappings.conf y "
"hace que el daemon la recargue. Las entradas de menú <i>Exportar "
"configuración...</i> e <i>Importar configuración...</i> hacen lo mismo con "
"el archivo que elijas."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. "
"<i>Options</i> shows the same in more detail."
msgstr ""
"<b>Estado del daemon</b> – el pie de ventana muestra si gkeymapper-daemon "
"está en ejecución, qué teclado ha encontrado y la última tecla G que se ha "
"pulsado. <i>Opciones</i> muestra lo mismo con más detalle."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> only opens this window when "
"you log in. The G keys work without it: gkeymapper-daemon (a user service) "
"reads them and replays the actions, and that daemon is the only part that has "
"to run."
msgstr ""
"<b>Inicio automático</b> – <i>Iniciar con la sesión</i> solo abre esta "
"ventana. Las teclas G funcionan sin ella: gkeymapper-daemon (un servicio de "
"usuario) las lee y reproduce las acciones, y ese daemon es lo único que tiene "
"que estar en marcha."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved are lost "
"with it."
msgstr ""
"<b>Bandeja y ventana</b> – con <i>Minimizar a la bandeja</i>, al cerrar o "
"minimizar la ventana se oculta en lugar de cerrarse; el icono de la bandeja "
"la vuelve a mostrar (<i>Mostrar</i>) y cierra el programa (<i>Cerrar</i>). "
"Volver a iniciar gKeyMapper cierra la ventana antigua: las filas no "
"guardadas se pierden con ella."

326
po/fr.po
View File

@@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gkeymapper 0.1\n" "Project-Id-Version: gkeymapper 0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-18 03:08+0200\n" "POT-Creation-Date: 2026-10-18 20:18+0200\n"
"PO-Revision-Date: 2026-10-18 03:08+0200\n" "PO-Revision-Date: 2026-10-18 03:08+0200\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
@@ -13,14 +13,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgid "Choose file" msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "Choisir un fichier" msgstr "Assignateur de touches G pour le Logitech G910 (GTK4)."
msgid "_Cancel"
msgstr "_Annuler"
msgid "_Open"
msgstr "_Ouvrir"
msgid "No keys recorded yet" msgid "No keys recorded yet"
msgstr "Aucune touche enregistrée pour le moment" msgstr "Aucune touche enregistrée pour le moment"
@@ -28,6 +22,14 @@ msgstr "Aucune touche enregistrée pour le moment"
msgid "Capture key sequence" msgid "Capture key sequence"
msgstr "Capturer une séquence de touches" msgstr "Capturer une séquence de touches"
msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgstr ""
"Appuyez sur les touches dans l'ordre (les modificateurs comme Ctrl, Alt, "
"Maj, Super et AltGr sont également enregistrés). Cliquez sur Terminer pour "
"appliquer ou sur Annuler pour abandonner."
msgid "Clear" msgid "Clear"
msgstr "Effacer" msgstr "Effacer"
@@ -38,12 +40,122 @@ msgid "Done"
msgstr "Terminer" msgstr "Terminer"
msgid "" msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are " "<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
"recorded too). Click Done to apply or Cancel to discard." "the target and press <i>Save mappings</i>."
msgstr "" msgstr ""
"Appuyez sur les touches dans l'ordre (les modificateurs comme Ctrl, Alt, " "<b>Les neuf lignes</b> – une par touche G. Choisissez le type, saisissez ou "
"Maj, Super et AltGr sont également enregistrés). Cliquez sur Terminer pour " "capturez la cible puis cliquez sur <i>Enregistrer les attributions</i>."
"appliquer ou sur Annuler pour abandonner."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c."
msgstr "<b>Touche</b> – presse une fois la séquence capturée, par ex. ctrl+c."
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, e."
"g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Touche (maintenue)</b> – l'appuie et la maintient tant que la touche G "
"est maintenue, p. ex. pour appuyer-pour-parler. Une seule étape peut être "
"maintenue."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --"
"new-window."
msgstr ""
"<b>Commande</b> – lance un programme ou une ligne de commande, par ex. /usr/"
"bin/firefox --new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Désactivé</b> – la touche ne fait rien."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then <i>Done</"
"i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Capturer</b> – enregistre les touches que vous pressez ensuite : pressez-"
"les puis <i>Terminer</i>. Les modificateurs sont conservés (shift, altgr, …) "
"et les noms ne dépendent pas de la disposition, donc ctrl+z désigne la "
"touche qui tape z sur votre disposition."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Enregistrer les attributions</b> – écrit ~/.config/gKeyMapper/mappings."
"conf et fait recharger le fichier au démon. Les entrées de menu <i>Exporter "
"la configuration...</i> et <i>Importer la configuration...</i> font la même "
"chose avec le fichier de votre choix."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. <i>Options</"
"i> shows the same in more detail."
msgstr ""
"<b>État du démon</b> – le pied de fenêtre indique si gkeymapper-daemon est "
"en cours d'exécution, quel clavier a été trouvé et la dernière touche G "
"déclenchée. <i>Options</i> affiche la même chose plus en détail."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> opens this window minimized "
"to the tray when you log in. The G keys work without it: gkeymapper-daemon "
"(a user service) reads them and replays the actions, and that daemon is the "
"only part that has to run."
msgstr ""
"<b>Démarrage automatique</b> – <i>Démarrer avec la session</i> ouvre cette "
"fenêtre réduite dans la zone de notification à l'ouverture de la session. "
"Les touches G fonctionnent sans elle : gkeymapper-daemon (un service "
"utilisateur) les lit et rejoue les actions, et ce démon est la seule partie "
"qui doit tourner."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved yet come "
"along."
msgstr ""
"<b>Zone de notification et fenêtre</b> – avec <i>Réduire dans la zone de "
"notification</i>, la fenêtre est masquée au lieu d'être fermée lorsque vous "
"la fermez ou la réduisez ; l'icône de la zone de notification la réaffiche "
"(<i>Afficher</i>) et quitte le programme (<i>Fermer</i>). Relancer "
"gKeyMapper ferme l'ancienne fenêtre : les lignes pas encore enregistrées "
"sont conservées."
msgid "Help"
msgstr "Aide"
msgid "Close window"
msgstr "Fermer la fenêtre"
msgid "Daemon: not reachable"
msgstr "Démon : injoignable"
msgid "Daemon: running"
msgstr "Démon : en cours d'exécution"
msgid "Device: not found"
msgstr "Appareil : introuvable"
#, c-format
msgid "Device: %s"
msgstr "Appareil : %s"
msgid "Configuration error"
msgstr "Erreur de configuration"
#, c-format
msgid "Last key: %s"
msgstr "Dernière touche : %s"
msgid "Choose file"
msgstr "Choisir un fichier"
msgid "_Cancel"
msgstr "_Annuler"
msgid "_Open"
msgstr "_Ouvrir"
msgid "Choose file..." msgid "Choose file..."
msgstr "Choisir un fichier..." msgstr "Choisir un fichier..."
@@ -54,12 +166,12 @@ msgstr "ex. /usr/bin/firefox ou commande personnalisée"
msgid "Capture" msgid "Capture"
msgstr "Capturer" msgstr "Capturer"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "ex. ctrl+c, ctrl+v"
msgid "e.g. ctrl+w (held while the G key is held)" msgid "e.g. ctrl+w (held while the G key is held)"
msgstr "ex. ctrl+w (maintenue tant que la touche G est enfoncée)" msgstr "ex. ctrl+w (maintenue tant que la touche G est enfoncée)"
msgid "e.g. ctrl+c, ctrl+v"
msgstr "ex. ctrl+c, ctrl+v"
#, c-format #, c-format
msgid "Could not save %s.\n" msgid "Could not save %s.\n"
msgstr "Impossible d'enregistrer %s.\n" msgstr "Impossible d'enregistrer %s.\n"
@@ -67,19 +179,12 @@ msgstr "Impossible d'enregistrer %s.\n"
msgid "Mappings saved to " msgid "Mappings saved to "
msgstr "Attributions enregistrées dans " msgstr "Attributions enregistrées dans "
#, c-format
msgid "Could not open %s.\n"
msgstr "Impossible d'ouvrir %s.\n"
msgid "Could not export mappings.\n" msgid "Could not export mappings.\n"
msgstr "Impossible d'exporter les attributions.\n" msgstr "Impossible d'exporter les attributions.\n"
msgid "gKeyMapper" msgid "gKeyMapper"
msgstr "gKeyMapper" msgstr "gKeyMapper"
msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr "Assignateur de touches G pour le Logitech G910 (GTK4)."
msgid "Export configuration" msgid "Export configuration"
msgstr "Exporter la configuration" msgstr "Exporter la configuration"
@@ -98,11 +203,19 @@ msgstr "Exporter la configuration..."
msgid "Import configuration..." msgid "Import configuration..."
msgstr "Importer la configuration..." msgstr "Importer la configuration..."
msgid "Options"
msgstr "Options"
msgid "About" msgid "About"
msgstr "À propos" msgstr "À propos"
msgid "Quit"
msgstr "Quitter"
msgid "G1 to G9 of the Logitech G910 remapped to keys or commands" msgid "G1 to G9 of the Logitech G910 remapped to keys or commands"
msgstr "Réassignation des touches G1 à G9 du Logitech G910 vers des touches ou des commandes" msgstr ""
"Réassignation des touches G1 à G9 du Logitech G910 vers des touches ou des "
"commandes"
msgid "G-Key" msgid "G-Key"
msgstr "Touche G" msgstr "Touche G"
@@ -134,44 +247,12 @@ msgstr "Enregistrer les attributions"
msgid "Saved to ~/.config/gKeyMapper/mappings.conf" msgid "Saved to ~/.config/gKeyMapper/mappings.conf"
msgstr "Enregistré dans ~/.config/gKeyMapper/mappings.conf" msgstr "Enregistré dans ~/.config/gKeyMapper/mappings.conf"
msgid "Daemon: not reachable"
msgstr "Démon : injoignable"
msgid "Daemon: running"
msgstr "Démon : en cours d'exécution"
msgid "Device: %s"
msgstr "Appareil : %s"
msgid "Device: not found"
msgstr "Appareil : introuvable"
msgid "Configuration error"
msgstr "Erreur de configuration"
msgid "Last key: %s"
msgstr "Dernière touche : %s"
msgid "Daemon: checking..." msgid "Daemon: checking..."
msgstr "Démon : vérification..." msgstr "Démon : vérification..."
msgid "Options" #, c-format
msgstr "Options" msgid "Could not open %s.\n"
msgstr "Impossible d'ouvrir %s.\n"
msgid "Help"
msgstr "Aide"
msgid "Quit"
msgstr "Quitter"
msgid "Show"
msgstr "Afficher"
msgid "Close"
msgstr "Fermer"
msgid "Close window"
msgstr "Fermer la fenêtre"
msgid "Window" msgid "Window"
msgstr "Fenêtre" msgstr "Fenêtre"
@@ -179,13 +260,16 @@ msgstr "Fenêtre"
msgid "Minimize to tray" msgid "Minimize to tray"
msgstr "Réduire dans la zone de notification" msgstr "Réduire dans la zone de notification"
msgid "Start with the session"
msgstr "Démarrer avec la session"
msgid "" msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon " "Opens this window minimized to the tray when you log in. Not needed for the "
"shows it again (Show) and quits the program (Close)." "G keys - the daemon does the work and is the only part that has to run."
msgstr "" msgstr ""
"À la fermeture ou à la réduction, la fenêtre est masquée au lieu de quitter. " "Ouvre cette fenêtre réduite dans la zone de notification à l'ouverture de la "
"L'icône de la zone de notification la réaffiche (Afficher) et quitte le " "session. Inutile pour les touches G : c'est le démon qui fait le travail, et "
"programme (Fermer)." "lui seul doit tourner."
msgid "Language" msgid "Language"
msgstr "Langue" msgstr "Langue"
@@ -198,21 +282,6 @@ msgstr ""
"L'interface change immédiatement, y compris le menu de la zone de " "L'interface change immédiatement, y compris le menu de la zone de "
"notification." "notification."
msgid "Start with the session"
msgstr "Démarrer avec la session"
msgid ""
"Opens this window when you log in. Not needed for the G keys - the daemon "
"does the work and is the only part that has to run."
msgstr ""
"Ouvre cette fenêtre à l'ouverture de la session. Inutile pour les touches G : "
"c'est le démon qui fait le travail, et lui seul doit tourner."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
"Ce bureau n'a pas de zone de notification, la fenêtre ne peut donc pas être "
"masquée."
msgid "Daemon status" msgid "Daemon status"
msgstr "État du démon" msgstr "État du démon"
@@ -234,6 +303,19 @@ msgstr "Fichier de configuration"
msgid "Last key" msgid "Last key"
msgstr "Dernière touche" msgstr "Dernière touche"
msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgstr ""
"À la fermeture ou à la réduction, la fenêtre est masquée au lieu de quitter. "
"L'icône de la zone de notification la réaffiche (Afficher) et quitte le "
"programme (Fermer)."
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
"Ce bureau n'a pas de zone de notification, la fenêtre ne peut donc pas être "
"masquée."
msgid "active" msgid "active"
msgstr "actif" msgstr "actif"
@@ -249,84 +331,8 @@ msgstr "inaccessible"
msgid "not found" msgid "not found"
msgstr "introuvable" msgstr "introuvable"
msgid "" msgid "Show"
"<b>The nine rows</b> – one per G key. Choose the type, then enter or capture " msgstr "Afficher"
"the target and press <i>Save mappings</i>."
msgstr ""
"<b>Les neuf lignes</b> – une par touche G. Choisissez le type, saisissez ou "
"capturez la cible puis cliquez sur <i>Enregistrer les attributions</i>."
msgid "<b>Key</b> – presses the captured sequence once, e.g. ctrl+c." msgid "Close"
msgstr "<b>Touche</b> – presse une fois la séquence capturée, par ex. ctrl+c." msgstr "Fermer"
msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, "
"e.g. for push-to-talk. Only a single step can be held."
msgstr ""
"<b>Touche (maintenue)</b> – l'appuie et la maintient tant que la touche G "
"est maintenue, p. ex. pour appuyer-pour-parler. Une seule étape peut être "
"maintenue."
msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox "
"--new-window."
msgstr ""
"<b>Commande</b> – lance un programme ou une ligne de commande, par ex. "
"/usr/bin/firefox --new-window."
msgid "<b>Disabled</b> – the key does nothing."
msgstr "<b>Désactivé</b> – la touche ne fait rien."
msgid ""
"<b>Capture</b> – records the keys you press next: press them, then "
"<i>Done</i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout."
msgstr ""
"<b>Capturer</b> – enregistre les touches que vous pressez ensuite : "
"pressez-les puis <i>Terminer</i>. Les modificateurs sont conservés (shift, "
"altgr, …) et les noms ne dépendent pas de la disposition, donc ctrl+z "
"désigne la touche qui tape z sur votre disposition."
msgid ""
"<b>Save mappings</b> – writes ~/.config/gKeyMapper/mappings.conf and makes "
"the daemon reload it. The menu entries <i>Export configuration...</i> and "
"<i>Import configuration...</i> do the same with a file you choose."
msgstr ""
"<b>Enregistrer les attributions</b> – écrit "
"~/.config/gKeyMapper/mappings.conf et fait recharger le fichier au démon. "
"Les entrées de menu <i>Exporter la configuration...</i> et <i>Importer la "
"configuration...</i> font la même chose avec le fichier de votre choix."
msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. "
"<i>Options</i> shows the same in more detail."
msgstr ""
"<b>État du démon</b> – le pied de fenêtre indique si gkeymapper-daemon est "
"en cours d'exécution, quel clavier a été trouvé et la dernière touche G "
"déclenchée. <i>Options</i> affiche la même chose plus en détail."
msgid ""
"<b>Autostart</b> – <i>Start with the session</i> only opens this window when "
"you log in. The G keys work without it: gkeymapper-daemon (a user service) "
"reads them and replays the actions, and that daemon is the only part that has "
"to run."
msgstr ""
"<b>Démarrage automatique</b> – <i>Démarrer avec la session</i> ouvre "
"seulement cette fenêtre. Les touches G fonctionnent sans elle : "
"gkeymapper-daemon (un service utilisateur) les lit et rejoue les actions, et "
"ce démon est la seule partie qui doit tourner."
msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved are lost "
"with it."
msgstr ""
"<b>Zone de notification et fenêtre</b> – avec <i>Réduire dans la zone de "
"notification</i>, la fenêtre est masquée au lieu d'être fermée lorsque vous "
"la fermez ou la réduisez ; l'icône de la zone de notification la réaffiche "
"(<i>Afficher</i>) et quitte le programme (<i>Fermer</i>). Relancer "
"gKeyMapper ferme l'ancienne fenêtre : les lignes non enregistrées sont "
"perdues avec elle."

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-18 03:08+0200\n" "POT-Creation-Date: 2026-10-18 20:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,16 +17,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: src/main.cpp:126 #: src/autostart.cpp:97 src/main.cpp:280 src/trayicon.cpp:371
msgid "Choose file" msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr ""
#: src/main.cpp:129 src/main.cpp:300 src/main.cpp:312
msgid "_Cancel"
msgstr ""
#: src/main.cpp:130 src/main.cpp:313
msgid "_Open"
msgstr "" msgstr ""
#: src/capturedialog.cpp:115 src/capturedialog.cpp:226 #: src/capturedialog.cpp:115 src/capturedialog.cpp:226
@@ -37,6 +29,12 @@ msgstr ""
msgid "Capture key sequence" msgid "Capture key sequence"
msgstr "" msgstr ""
#: src/capturedialog.cpp:220
msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgstr ""
#: src/capturedialog.cpp:250 #: src/capturedialog.cpp:250
msgid "Clear" msgid "Clear"
msgstr "" msgstr ""
@@ -49,270 +47,6 @@ msgstr ""
msgid "Done" msgid "Done"
msgstr "" msgstr ""
#: src/capturedialog.cpp:220
msgid ""
"Press keys in order (modifiers like Ctrl, Alt, Shift, Super and AltGr are "
"recorded too). Click Done to apply or Cancel to discard."
msgstr ""
#: src/main.cpp:160
msgid "Choose file..."
msgstr ""
#: src/main.cpp:162
msgid "e.g. /usr/bin/firefox or custom command"
msgstr ""
#: src/main.cpp:164 src/main.cpp:168 src/main.cpp:685
msgid "Capture"
msgstr ""
#: src/main.cpp:170 src/main.cpp:678
msgid "e.g. ctrl+c, ctrl+v"
msgstr ""
#: src/main.cpp:166
msgid "e.g. ctrl+w (held while the G key is held)"
msgstr ""
#: src/main.cpp:185
#, c-format
msgid "Could not save %s.\n"
msgstr ""
#: src/main.cpp:193
msgid "Mappings saved to "
msgstr ""
#: src/mappings.cpp:41
#, c-format
msgid "Could not open %s.\n"
msgstr ""
#: src/main.cpp:203
msgid "Could not export mappings.\n"
msgstr ""
#: src/main.cpp:264 src/main.cpp:590 src/trayicon.cpp:370 src/trayicon.cpp:500
msgid "gKeyMapper"
msgstr ""
#: src/autostart.cpp:97 src/main.cpp:267 src/trayicon.cpp:371
msgid "G-Key mapper for the Logitech G910 (GTK4)."
msgstr ""
#: src/main.cpp:297
msgid "Export configuration"
msgstr ""
#: src/main.cpp:301
msgid "_Save"
msgstr ""
#: src/main.cpp:309
msgid "Import configuration"
msgstr ""
#: src/main.cpp:575
msgid "gKeyMapper – Logitech G910"
msgstr ""
#: src/main.cpp:594
msgid "Export configuration..."
msgstr ""
#: src/main.cpp:595
msgid "Import configuration..."
msgstr ""
#: src/main.cpp:603
msgid "About"
msgstr ""
#: src/main.cpp:623
msgid "G1 to G9 of the Logitech G910 remapped to keys or commands"
msgstr ""
#: src/main.cpp:644
msgid "G-Key"
msgstr ""
#: src/main.cpp:644
msgid "Type"
msgstr ""
#: src/main.cpp:644
msgid "Target (Key/Command)"
msgstr ""
#: src/main.cpp:644
msgid "Action"
msgstr ""
#: src/main.cpp:653
msgid "Key"
msgstr ""
#: src/main.cpp:653
msgid "Command"
msgstr ""
#: src/main.cpp:653
msgid "Disabled"
msgstr ""
#: src/main.cpp:654
msgid "Key (hold)"
msgstr ""
#: src/main.cpp:713
msgid "Save mappings"
msgstr ""
#: src/main.cpp:720
msgid "Saved to ~/.config/gKeyMapper/mappings.conf"
msgstr ""
#: src/main.cpp:63
msgid "Daemon: not reachable"
msgstr ""
#: src/main.cpp:65
msgid "Daemon: running"
msgstr ""
#: src/main.cpp:70
msgid "Device: not found"
msgstr ""
#: src/main.cpp:73
msgid "Device: %s"
msgstr ""
#: src/main.cpp:78 src/optionsdialog.cpp:212
msgid "Configuration error"
msgstr ""
#: src/main.cpp:83
msgid "Last key: %s"
msgstr ""
#: src/main.cpp:731
msgid "Daemon: checking..."
msgstr ""
#: src/main.cpp:598 src/optionsdialog.cpp:81
msgid "Options"
msgstr ""
#: src/help.cpp:67 src/help.cpp:86 src/main.cpp:602
msgid "Help"
msgstr ""
#: src/main.cpp:609
msgid "Quit"
msgstr ""
#: src/trayicon.cpp:136
msgid "Show"
msgstr ""
#: src/trayicon.cpp:139
msgid "Close"
msgstr ""
#: src/help.cpp:95 src/optionsdialog.cpp:164
msgid "Close window"
msgstr ""
#: src/optionsdialog.cpp:95
msgid "Window"
msgstr ""
#: src/optionsdialog.cpp:97
msgid "Minimize to tray"
msgstr ""
#: src/optionsdialog.cpp:200
msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgstr ""
#: src/optionsdialog.cpp:128 src/optionsdialog.cpp:136
msgid "Language"
msgstr ""
#: src/optionsdialog.cpp:131
msgid "System language"
msgstr ""
#: src/optionsdialog.cpp:141
msgid "The interface switches right away, including the tray menu."
msgstr ""
#: src/optionsdialog.cpp:110
msgid "Start with the session"
msgstr ""
#: src/optionsdialog.cpp:122
msgid ""
"Opens this window when you log in. Not needed for the G keys - the daemon "
"does the work and is the only part that has to run."
msgstr ""
#: src/optionsdialog.cpp:202
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
#: src/optionsdialog.cpp:144
msgid "Daemon status"
msgstr ""
#: src/optionsdialog.cpp:157
msgid "Tray icon"
msgstr ""
#: src/optionsdialog.cpp:158
msgid "State"
msgstr ""
#: src/optionsdialog.cpp:159
msgid "Version"
msgstr ""
#: src/optionsdialog.cpp:160
msgid "Device"
msgstr ""
#: src/optionsdialog.cpp:161
msgid "Configuration file"
msgstr ""
#: src/optionsdialog.cpp:162
msgid "Last key"
msgstr ""
#: src/optionsdialog.cpp:205
msgid "active"
msgstr ""
#: src/optionsdialog.cpp:205
msgid "not available"
msgstr ""
#: src/optionsdialog.cpp:206
msgid "running"
msgstr ""
#: src/optionsdialog.cpp:206
msgid "not reachable"
msgstr ""
#: src/optionsdialog.cpp:208
msgid "not found"
msgstr ""
#: src/help.cpp:34 #: src/help.cpp:34
msgid "" msgid ""
"<b>The nine rows</b> – one per G key. Choose the type, then enter or capture " "<b>The nine rows</b> – one per G key. Choose the type, then enter or capture "
@@ -325,14 +59,14 @@ msgstr ""
#: src/help.cpp:37 #: src/help.cpp:37
msgid "" msgid ""
"<b>Key (hold)</b> – presses it and keeps it down while the G key is held, " "<b>Key (hold)</b> – presses it and keeps it down while the G key is held, e."
"e.g. for push-to-talk. Only a single step can be held." "g. for push-to-talk. Only a single step can be held."
msgstr "" msgstr ""
#: src/help.cpp:39 #: src/help.cpp:39
msgid "" msgid ""
"<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox " "<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --"
"--new-window." "new-window."
msgstr "" msgstr ""
#: src/help.cpp:40 #: src/help.cpp:40
@@ -341,8 +75,8 @@ msgstr ""
#: src/help.cpp:41 #: src/help.cpp:41
msgid "" msgid ""
"<b>Capture</b> – records the keys you press next: press them, then " "<b>Capture</b> – records the keys you press next: press them, then <i>Done</"
"<i>Done</i>. Modifiers are kept (shift, altgr, …) and the names are layout " "i>. Modifiers are kept (shift, altgr, …) and the names are layout "
"independent, so ctrl+z means the key that types z on your layout." "independent, so ctrl+z means the key that types z on your layout."
msgstr "" msgstr ""
@@ -356,16 +90,16 @@ msgstr ""
#: src/help.cpp:47 #: src/help.cpp:47
msgid "" msgid ""
"<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is " "<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is "
"running, which keyboard it found and the last G key that fired. " "running, which keyboard it found and the last G key that fired. <i>Options</"
"<i>Options</i> shows the same in more detail." "i> shows the same in more detail."
msgstr "" msgstr ""
#: src/help.cpp:50 #: src/help.cpp:50
msgid "" msgid ""
"<b>Autostart</b> – <i>Start with the session</i> only opens this window when " "<b>Autostart</b> – <i>Start with the session</i> opens this window minimized "
"you log in. The G keys work without it: gkeymapper-daemon (a user service) " "to the tray when you log in. The G keys work without it: gkeymapper-daemon "
"reads them and replays the actions, and that daemon is the only part that has " "(a user service) reads them and replays the actions, and that daemon is the "
"to run." "only part that has to run."
msgstr "" msgstr ""
#: src/help.cpp:53 #: src/help.cpp:53
@@ -373,6 +107,274 @@ msgid ""
"<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden " "<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden "
"instead of closed when you close or minimize it; the tray icon shows it " "instead of closed when you close or minimize it; the tray icon shows it "
"again (<i>Show</i>) and quits the program (<i>Close</i>). Starting " "again (<i>Show</i>) and quits the program (<i>Close</i>). Starting "
"gKeyMapper again closes the old window – rows that were not saved are lost " "gKeyMapper again closes the old window – rows that were not saved yet come "
"with it." "along."
msgstr ""
#: src/help.cpp:67 src/help.cpp:86 src/main.cpp:635
msgid "Help"
msgstr ""
#: src/help.cpp:95 src/optionsdialog.cpp:177
msgid "Close window"
msgstr ""
#: src/main.cpp:76
msgid "Daemon: not reachable"
msgstr ""
#: src/main.cpp:78
msgid "Daemon: running"
msgstr ""
#: src/main.cpp:83
msgid "Device: not found"
msgstr ""
#: src/main.cpp:86
#, c-format
msgid "Device: %s"
msgstr ""
#: src/main.cpp:91 src/optionsdialog.cpp:228
msgid "Configuration error"
msgstr ""
#: src/main.cpp:96
#, c-format
msgid "Last key: %s"
msgstr ""
#: src/main.cpp:139
msgid "Choose file"
msgstr ""
#: src/main.cpp:142 src/main.cpp:313 src/main.cpp:325
msgid "_Cancel"
msgstr ""
#: src/main.cpp:143 src/main.cpp:326
msgid "_Open"
msgstr ""
#: src/main.cpp:173
msgid "Choose file..."
msgstr ""
#: src/main.cpp:175
msgid "e.g. /usr/bin/firefox or custom command"
msgstr ""
#: src/main.cpp:177 src/main.cpp:181 src/main.cpp:726
msgid "Capture"
msgstr ""
#: src/main.cpp:179
msgid "e.g. ctrl+w (held while the G key is held)"
msgstr ""
#: src/main.cpp:183 src/main.cpp:719
msgid "e.g. ctrl+c, ctrl+v"
msgstr ""
#: src/main.cpp:198
#, c-format
msgid "Could not save %s.\n"
msgstr ""
#: src/main.cpp:206
msgid "Mappings saved to "
msgstr ""
#: src/main.cpp:216
msgid "Could not export mappings.\n"
msgstr ""
#: src/main.cpp:277 src/main.cpp:623 src/trayicon.cpp:370 src/trayicon.cpp:500
msgid "gKeyMapper"
msgstr ""
#: src/main.cpp:310
msgid "Export configuration"
msgstr ""
#: src/main.cpp:314
msgid "_Save"
msgstr ""
#: src/main.cpp:322
msgid "Import configuration"
msgstr ""
#: src/main.cpp:608
msgid "gKeyMapper – Logitech G910"
msgstr ""
#: src/main.cpp:627
msgid "Export configuration..."
msgstr ""
#: src/main.cpp:628
msgid "Import configuration..."
msgstr ""
#: src/main.cpp:631 src/optionsdialog.cpp:83
msgid "Options"
msgstr ""
#: src/main.cpp:636
msgid "About"
msgstr ""
#: src/main.cpp:642
msgid "Quit"
msgstr ""
#: src/main.cpp:656
msgid "G1 to G9 of the Logitech G910 remapped to keys or commands"
msgstr ""
#: src/main.cpp:677
msgid "G-Key"
msgstr ""
#: src/main.cpp:677
msgid "Type"
msgstr ""
#: src/main.cpp:677
msgid "Target (Key/Command)"
msgstr ""
#: src/main.cpp:677
msgid "Action"
msgstr ""
#: src/main.cpp:689
msgid "Key"
msgstr ""
#: src/main.cpp:689
msgid "Command"
msgstr ""
#: src/main.cpp:689
msgid "Disabled"
msgstr ""
#: src/main.cpp:690
msgid "Key (hold)"
msgstr ""
#: src/main.cpp:761
msgid "Save mappings"
msgstr ""
#: src/main.cpp:768
msgid "Saved to ~/.config/gKeyMapper/mappings.conf"
msgstr ""
#: src/main.cpp:779
msgid "Daemon: checking..."
msgstr ""
#: src/mappings.cpp:41
#, c-format
msgid "Could not open %s.\n"
msgstr ""
#: src/optionsdialog.cpp:97
msgid "Window"
msgstr ""
#: src/optionsdialog.cpp:99
msgid "Minimize to tray"
msgstr ""
#: src/optionsdialog.cpp:112
msgid "Start with the session"
msgstr ""
#: src/optionsdialog.cpp:124
msgid ""
"Opens this window minimized to the tray when you log in. Not needed for the "
"G keys - the daemon does the work and is the only part that has to run."
msgstr ""
#: src/optionsdialog.cpp:131 src/optionsdialog.cpp:149
msgid "Language"
msgstr ""
#: src/optionsdialog.cpp:137
msgid "System language"
msgstr ""
#: src/optionsdialog.cpp:154
msgid "The interface switches right away, including the tray menu."
msgstr ""
#: src/optionsdialog.cpp:157
msgid "Daemon status"
msgstr ""
#: src/optionsdialog.cpp:170
msgid "Tray icon"
msgstr ""
#: src/optionsdialog.cpp:171
msgid "State"
msgstr ""
#: src/optionsdialog.cpp:172
msgid "Version"
msgstr ""
#: src/optionsdialog.cpp:173
msgid "Device"
msgstr ""
#: src/optionsdialog.cpp:174
msgid "Configuration file"
msgstr ""
#: src/optionsdialog.cpp:175
msgid "Last key"
msgstr ""
#: src/optionsdialog.cpp:216
msgid ""
"Closing or minimizing the window hides it instead of quitting. The tray icon "
"shows it again (Show) and quits the program (Close)."
msgstr ""
#: src/optionsdialog.cpp:218
msgid "No tray is available on this desktop, so the window cannot be hidden."
msgstr ""
#: src/optionsdialog.cpp:221
msgid "active"
msgstr ""
#: src/optionsdialog.cpp:221
msgid "not available"
msgstr ""
#: src/optionsdialog.cpp:222
msgid "running"
msgstr ""
#: src/optionsdialog.cpp:222
msgid "not reachable"
msgstr ""
#: src/optionsdialog.cpp:224
msgid "not found"
msgstr ""
#: src/trayicon.cpp:136
msgid "Show"
msgstr ""
#: src/trayicon.cpp:139
msgid "Close"
msgstr "" msgstr ""

535
scripts/i18n.sh Executable file
View File

@@ -0,0 +1,535 @@
#!/usr/bin/env bash
#
# gKeyMapper - everything that touches po/*.po, po/gkeymapper.pot and the compiled
# catalogs, in one place, so a translation change is a command instead of a list of
# steps to remember. The rules behind it are written down in po/README.md.
#
# Usage: scripts/i18n.sh <command> [arguments]
# extract regenerate po/gkeymapper.pot from the sources
# update [lang...] merge the pot into the catalogs: new strings come in
# untranslated, existing translations are kept
# check validate the pot, the catalogs and the language list
# status per-language overview (counts + compiled catalog)
# todo <lang> what is still untranslated or fuzzy, with the source
# locations from the pot
# new <lang> [name] create po/<lang>.po from the pot (name = Language-Team,
# e.g. "Italiano"); "new en" writes the identity catalog
# help this text
#
# Only gettext tools and coreutils are used. The tools can be overridden from the
# environment, like the Makefile's variables: XGETTEXT=... scripts/i18n.sh extract
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
# Everything is addressed relative to the repository root and run from there: the
# source locations xgettext writes into the pot ("src/main.cpp:582") and the file
# names the gettext tools print are relative to the current directory, and absolute
# paths would end up in the pot and in every message this script shows.
cd -- "$ROOT" || exit 1
PO_DIR="po"
POT="$PO_DIR/gkeymapper.pot"
LOCALE_DIR="locale"
PACKAGE="gkeymapper"
# What the catalogs carry in Project-Id-Version (see po/*.po).
PO_VERSION="0.1"
XGETTEXT=${XGETTEXT:-xgettext}
MSGFMT=${MSGFMT:-msgfmt}
MSGMERGE=${MSGMERGE:-msgmerge}
MSGATTRIB=${MSGATTRIB:-msgattrib}
MSGCAT=${MSGCAT:-msgcat}
# The counts and message lists below are parsed, so the tools have to answer in
# English, whatever the session's language is.
export LC_ALL=C
# A temp file for a pot, removed on every exit - also on the error paths, where it
# is the half-written file that must not stay behind.
POT_TMP=""
trap 'rm -f "${POT_TMP:-}"' EXIT
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
die() {
printf 'i18n: %s\n' "$*" >&2
exit 1
}
note() {
printf '%s\n' "$*"
}
require_tools() {
local tool
for tool in "$@"; do
command -v "$tool" >/dev/null 2>&1 ||
die "$tool not found - install gettext (see README, Requirements)"
done
}
# Every source that may hold translatable strings: the GUI, which is the only
# program with gettext set up (src/main.cpp) - the same two directories the Makefile
# compiles into it. The daemon is English on purpose (see `check`), so its sources
# are not scanned. The pot is generated from this list, in this order, so `extract`
# is reproducible: a hand-ordered file list once made the pot's message order depend
# on the machine it ran on.
sources() {
local file
for file in "$ROOT"/src/*.cpp "$ROOT"/shared/*.cpp; do
if [ -e "$file" ]; then printf '%s\n' "${file#"$ROOT"/}"; fi
done
}
# The languages that have a catalog, i.e. po/*.po - the same list the Makefile
# compiles and installs.
languages() {
local file
for file in "$PO_DIR"/*.po; do
if [ -e "$file" ]; then basename "$file" .po; fi
done | sort
}
# The messages of a po/pot, one per line and multi-line strings joined: msgcat does
# the joining, so `_("a" "b")` in the sources and the pot entry are the same string.
# The header (empty msgid) is left out.
msgids() {
"$MSGCAT" --stringtable-output "$1" 2>/dev/null |
sed -n 's/^"\(.*\)" = .*$/\1/p' | sed '/^$/d'
}
# translated/fuzzy/untranslated of one catalog, into three globals. msgfmt prints
# them as a comma separated list ("81 translated messages, 1 untranslated
# message.") and leaves the parts out that are zero, so the list is split first -
# a pattern like ".*\([0-9]\+\) untranslated" would stop at the last digit of a
# count and read 82 as 2.
stats() {
local out parts
out=$("$MSGFMT" --statistics -o /dev/null "$1" 2>&1) || die "$MSGFMT rejected $1"
parts=$(printf '%s\n' "$out" | tr ',' '\n')
translated=$(printf '%s\n' "$parts" | sed -n 's/^ *\([0-9]\+\) translated.*/\1/p'); : "${translated:=0}"
fuzzy=$(printf '%s\n' "$parts" | sed -n 's/^ *\([0-9]\+\) fuzzy.*/\1/p'); : "${fuzzy:=0}"
untranslated=$(printf '%s\n' "$parts" | sed -n 's/^ *\([0-9]\+\) untranslated.*/\1/p'); : "${untranslated:=0}"
}
# The `Language:` header of a catalog, so a copy-pasted file is noticed.
po_language() {
sed -n 's/^"Language: \(.*\)\\n"$/\1/p' "$1"
}
# The POT-Creation-Date of a po/pot. It does not decide whether a file is up to
# date (the message set does); it tells whether a merge happened after the last
# extract.
pot_date() {
sed -n 's/^"POT-Creation-Date: \(.*\)\\n"$/\1/p' "$1"
}
# One line per entry whose msgstr is not the msgid again - the rule for po/en.po,
# which is an identity catalog because English is the source language. Compared as
# raw (still escaped) text: both sides come from the same file, so the escaping
# matches. Plurals need more than this and are not used anywhere (no ngettext).
identity_mismatches() {
awk '
function quoted(line) { sub(/^[^"]*"/, "", line); sub(/"$/, "", line); return line }
function check() { if (id != "" && str != id) { print id; mismatches++ } }
/^msgid_plural|^msgstr\[/ { print "plural entries are not supported"; exit 2 }
/^msgid / { check(); id = quoted($0); str = ""; in_id = 1; next }
/^msgstr / { str = quoted($0); in_id = 0; next }
/^"/ { if (in_id) id = id quoted($0); else str = str quoted($0); next }
/^#/ { next }
/^[[:space:]]*$/ { next }
END { check(); exit (mismatches > 0 ? 1 : 0) }
' "$1"
}
# The pot as xgettext writes it right now, into the given file. `_()` marks
# strings that are translated when they are used, `N_()` the ones that are only
# collected (the help text is assembled later on); both are named explicitly,
# because a gettext that does not know `_` by itself is not unheard of.
extract_to() {
local out="$1" file
require_tools "$XGETTEXT"
local -a files=()
while IFS= read -r file; do files+=("$file"); done < <(sources)
[ "${#files[@]}" -gt 0 ] || die "no sources found under src/ or shared/"
"$XGETTEXT" --from-code=UTF-8 --language=C++ --keyword=_ --keyword=N_ \
--output="$out" "${files[@]}" || die "$XGETTEXT failed"
# Nothing or header-only means the extraction is broken, never that the program
# has no strings. Checked before the pot is replaced, so a broken gettext
# cannot empty a good pot.
[ -s "$out" ] || die "$XGETTEXT produced no output (is gettext working? $XGETTEXT --version)"
[ "$(grep -c '^msgid "' "$out")" -gt 1 ] ||
die "$XGETTEXT extracted no message - are the keywords right?"
}
# The pot without its header entry and without source locations: the entries of a
# fresh catalog. With identity=1 every msgstr repeats its msgid (po/en.po).
pot_body() {
awk -v identity="$2" '
NR == 1 { skipping = 1 }
skipping { if ($0 ~ /^$/) skipping = 0; next }
/^#:/ { next }
/^msgid / {
if (identity) { buffer = $0; sub(/^msgid/, "msgstr", buffer); collecting = 1 }
print; next
}
/^"/ { if (collecting) buffer = buffer "\n" $0; print; next }
/^msgstr ""$/ {
if (collecting) { print buffer; collecting = 0; next }
print; next
}
{ print }
' "$1"
}
# The language codes the GUI offers (src/optionsdialog.cpp, without the leading
# "" for "follow the session"). A catalog that is not in this list cannot be
# picked; a code without a catalog is a window that stays English.
gui_language_codes() {
sed -n 's/.*kLanguageCodes\[\] *= *{\(.*\)};.*/\1/p' "src/optionsdialog.cpp" |
tr ',' '\n' | tr -d ' "' | sed '/^$/d'
}
# The labels of that dropdown: one more than the codes, the first being
# "System language".
gui_language_labels() {
sed -n '/languageOptions\[\] *= *{/,/nullptr/p' "src/optionsdialog.cpp" |
sed 's/.*{//; s/nullptr.*//' | tr ',' '\n' | grep -c '[^[:space:]]'
}
# ---------------------------------------------------------------------------
# commands
# ---------------------------------------------------------------------------
cmd_extract() {
POT_TMP=$(mktemp "${TMPDIR:-/tmp}/$PACKAGE-pot.XXXXXX")
extract_to "$POT_TMP"
if [ -f "$POT" ]; then
local added gone
added=$(comm -13 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
gone=$(comm -23 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
note "po/gkeymapper.pot: $(msgids "$POT_TMP" | wc -l) messages"
[ -z "$added" ] || { note "new:"; printf '%s\n' "$added" | sed 's/^/ /'; }
[ -z "$gone" ] || { note "gone:"; printf '%s\n' "$gone" | sed 's/^/ /'; }
else
note "po/gkeymapper.pot: $(msgids "$POT_TMP" | wc -l) messages"
fi
mv "$POT_TMP" "$POT"
POT_TMP="" # moved, nothing to clean up
note "wrote po/gkeymapper.pot - now: scripts/i18n.sh update"
}
cmd_update() {
local lang po
require_tools "$MSGMERGE" "$MSGFMT"
[ -f "$POT" ] || die "$POT is missing - run: scripts/i18n.sh extract"
local -a targets
if [ "$#" -gt 0 ]; then targets=("$@"); else mapfile -t targets < <(languages); fi
[ "${#targets[@]}" -gt 0 ] || die "no po/*.po to update"
for lang in "${targets[@]}"; do
po="$PO_DIR/$lang.po"
[ -f "$po" ] || die "$po does not exist (a new language: scripts/i18n.sh new $lang)"
# --no-location: the catalogs carry no "#: file:line" comments on purpose -
# they are noise for translators and churn whenever a line moves. The
# locations live in the pot, and `todo` shows them from there.
"$MSGMERGE" --quiet --update --no-location "$po" "$POT"
# msgmerge keeps the previous file as <po>~; a backup in git is noise.
rm -f "$po~"
stats "$po"
note "po/$lang.po: $translated translated, $fuzzy fuzzy, $untranslated untranslated"
done
}
cmd_check() {
local lang po code out failures=0 stale_dates=""
require_tools "$MSGFMT" "$MSGATTRIB" "$MSGCAT" "$MSGMERGE" "$XGETTEXT"
[ -f "$POT" ] || die "$POT is missing"
local langs
langs=$(languages)
[ -n "$langs" ] || die "no po/*.po found"
# 1. Is the pot what the sources say? A message that was added or changed
# without touching the pot is what this catches - the source locations only
# drift when a line moves, which is worth a note, not a failure.
POT_TMP=$(mktemp "${TMPDIR:-/tmp}/$PACKAGE-pot.XXXXXX")
extract_to "$POT_TMP"
local added gone
added=$(comm -13 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
gone=$(comm -23 <(msgids "$POT" | sort) <(msgids "$POT_TMP" | sort))
if [ -n "$added" ] || [ -n "$gone" ]; then
note "po/gkeymapper.pot is out of date:"
[ -z "$added" ] || { note " in the sources, not in the pot:"; printf '%s\n' "$added" | sed 's/^/ /'; }
[ -z "$gone" ] || { note " in the pot, not in the sources:"; printf '%s\n' "$gone" | sed 's/^/ /'; }
note " fix: scripts/i18n.sh extract && scripts/i18n.sh update"
failures=$((failures + 1))
elif ! diff -q <(grep -v '^"POT-Creation-Date:' "$POT_TMP") \
<(grep -v '^"POT-Creation-Date:' "$POT") >/dev/null; then
note "po/gkeymapper.pot has the same messages, but an older extract (line numbers, order)"
note " only the locations in 'todo' are affected - scripts/i18n.sh extract refreshes them"
fi
# 2. Every catalog: parsable, complete, translated, and named what it says.
for lang in $langs; do
po="$PO_DIR/$lang.po"
# -c checks the format strings (%s, %d, ...) and the entry structure.
if ! out=$("$MSGFMT" -c -o /dev/null "$po" 2>&1); then
note "$lang: $MSGFMT -c failed:"
printf '%s\n' "$out" | sed 's/^/ /'
failures=$((failures + 1))
fi
# Does the catalog define the same messages as the pot? Compared as two
# lists rather than with msgcmp, which would also report every untranslated
# entry and drown the real problem (the counts below say what is missing).
local missing extra
missing=$(comm -23 <(msgids "$POT" | sort) <(msgids "$po" | sort))
extra=$(comm -13 <(msgids "$POT" | sort) <(msgids "$po" | sort))
if [ -n "$missing" ]; then
note "$lang: $po does not define:"
printf '%s\n' "$missing" | sed 's/^/ /'
failures=$((failures + 1))
fi
if [ -n "$extra" ]; then
note "$lang: $po defines messages the pot does not know:"
printf '%s\n' "$extra" | sed 's/^/ /'
failures=$((failures + 1))
fi
stats "$po"
if [ "$untranslated" -ne 0 ]; then
note "$lang: $untranslated untranslated message(s) - scripts/i18n.sh todo $lang"
failures=$((failures + 1))
fi
if [ "$fuzzy" -ne 0 ]; then
note "$lang: $fuzzy fuzzy message(s) - those are shown in English"
failures=$((failures + 1))
fi
# Strings that vanished from the program: msgmerge keeps them as "#~"
# entries. Not an error, but stale food for the next translator.
local obsolete=0
obsolete=$(grep -c '^#~' "$po" || true)
[ "$obsolete" -eq 0 ] ||
note "$lang: $obsolete obsolete entries (gone from the sources; msgattrib --no-obsolete drops them)"
if [ "$(po_language "$po")" != "$lang" ]; then
note "$lang: the header says Language: $(po_language "$po")"
failures=$((failures + 1))
fi
# English has a catalog of its own (an explicit language has to work on a
# German desktop) and it is an identity one.
if [ "$lang" = "en" ]; then
if ! out=$(identity_mismatches "$po"); then
note "en: po/en.po is not an identity catalog:"
printf '%s\n' "$out" | head -20 | sed 's/^/ /'
failures=$((failures + 1))
fi
fi
# A note, not a failure: it only means the file was not merged after the
# last extract, so its header carries an older date than the pot. Collected
# into one line - it says nothing about a single language.
if [ "$(pot_date "$po")" != "$(pot_date "$POT")" ]; then
stale_dates="$stale_dates $lang"
fi
done
[ -z "$stale_dates" ] ||
note "not merged against the current pot:$stale_dates (scripts/i18n.sh update)"
# 3. The languages the GUI offers (src/optionsdialog.cpp): a catalog that cannot
# be selected is dead weight, a code without a catalog is an English window
# with a German label.
local codes
codes=$(gui_language_codes)
[ -n "$codes" ] || die "could not read kLanguageCodes[] from src/optionsdialog.cpp"
for lang in $langs; do
if ! grep -qx "$lang" <<<"$codes"; then
note "po/$lang.po exists, but $lang is not offered in the options window"
note " add it to kLanguageCodes[] and languageOptions[] (src/optionsdialog.cpp)"
failures=$((failures + 1))
fi
done
for code in $codes; do
if [ ! -f "$PO_DIR/$code.po" ]; then
note "the options window offers $code, but po/$code.po is missing"
failures=$((failures + 1))
fi
done
if [ "$(gui_language_labels)" -ne "$(( $(printf '%s\n' "$codes" | wc -l) + 1 ))" ]; then
note "kLanguageCodes[] and languageOptions[] differ in length (src/optionsdialog.cpp)"
failures=$((failures + 1))
fi
# 4. Strings marked in the daemon. It has no gettext setup (no setlocale, no
# bindtextdomain - see daemon/main.cpp), so a `_()` there would end up in the
# pot, get translated, and still show English forever. The GUI is the
# translated program.
local daemon_marked
daemon_marked=$(grep -lE '(^|[^[:alnum:]_])_\("|N_\("' daemon/*.cpp 2>/dev/null || true)
if [ -n "$daemon_marked" ]; then
note "the daemon is not translated, but these files mark strings:"
printf '%s\n' "$daemon_marked" | sed 's/^/ /'
note " either drop the marker, or give the daemon its own gettext setup and"
note " add daemon/ to sources() in scripts/i18n.sh"
failures=$((failures + 1))
fi
if [ "$failures" -gt 0 ]; then
note "i18n: $failures problem(s)"
return 1
fi
note "i18n: $(printf '%s\n' "$langs" | wc -l) languages, $(msgids "$POT" | wc -l) messages, everything in sync"
}
cmd_status() {
local lang po mo mo_state
require_tools "$MSGFMT" "$MSGCAT"
[ -f "$POT" ] || die "$POT is missing"
printf '%-4s %-8s %10s %6s %11s %-9s %s\n' \
lang messages translated fuzzy untranslated catalog file
for lang in $(languages); do
po="$PO_DIR/$lang.po"
stats "$po"
mo="$LOCALE_DIR/$lang/LC_MESSAGES/$PACKAGE.mo"
mo_state="not built"
if [ -f "$mo" ]; then
# `make translations` rebuilds a catalog that is older than its po.
if [ "$mo" -nt "$po" ]; then mo_state="current"; else mo_state="stale"; fi
fi
printf '%-4s %-8s %10s %6s %11s %-9s %s\n' "$lang" \
"$((translated + fuzzy + untranslated))" "$translated" "$fuzzy" "$untranslated" \
"$mo_state" "po/$lang.po"
done
printf '\npot: %s messages, %s\n' "$(msgids "$POT" | wc -l)" "$(pot_date "$POT")"
}
cmd_todo() {
local lang="${1:-}"
[ -n "$lang" ] || die "usage: scripts/i18n.sh todo <lang>"
local po="$PO_DIR/$lang.po"
[ -f "$po" ] || die "$po does not exist"
require_tools "$MSGMERGE" "$MSGATTRIB"
# Merging the pot into the catalog is what puts the source locations next to
# the messages - the catalogs themselves have none (see cmd_update).
local open
open=$("$MSGMERGE" --quiet "$po" "$POT" | "$MSGATTRIB" --untranslated --no-obsolete)
if [ -n "$open" ]; then
# A valid po comment, so the output can be kept as a file and translated.
note "# --- untranslated in po/$lang.po ---"
printf '%s\n' "$open"
else
note "po/$lang.po: nothing untranslated"
fi
local fuzzy
fuzzy=$("$MSGMERGE" --quiet "$po" "$POT" | "$MSGATTRIB" --only-fuzzy --no-obsolete)
if [ -n "$fuzzy" ]; then
note
note "# --- fuzzy: shown in English until reviewed, the old text is kept as a comment ---"
printf '%s\n' "$fuzzy"
fi
}
cmd_new() {
local lang="${1:-}" team="${2:-}"
[ -n "$lang" ] || die "usage: scripts/i18n.sh new <lang> [Language-Team]"
case "$lang" in
*[!a-zA-Z_@.-]*) die "unexpected language code: $lang" ;;
esac
[ -f "$POT" ] || die "$POT is missing - run: scripts/i18n.sh extract"
local po="$PO_DIR/$lang.po"
[ -e "$po" ] && die "$po already exists"
if [ -z "$team" ]; then
# The two catalogs for a language that has been here from the start carry
# the English name of the language; anything else falls back to its code.
case "$lang" in
en) team="English" ;;
*) team="$lang" ;;
esac
fi
{
if [ "$lang" = "en" ]; then
printf '# English translation for %s.\n' "$PACKAGE"
printf '# English is the source language; this catalog keeps the strings explicit.\n'
else
printf '# %s translation for %s.\n' "$team" "$PACKAGE"
printf '# Copyright (C) %s\n' "$(date '+%Y')"
printf '# This file is distributed under the same license as the %s package.\n' "$PACKAGE"
fi
printf '#\n'
# The header in the shape the other catalogs use, so a new language does not
# start out looking different.
cat <<EOF
msgid ""
msgstr ""
"Project-Id-Version: $PACKAGE $PO_VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: $(pot_date "$POT")\n"
"PO-Revision-Date: $(date '+%Y-%m-%d %H:%M%z')\n"
"Last-Translator: \n"
"Language-Team: $team\n"
"Language: $lang\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
EOF
printf '\n'
# The entries come from the pot: it defines what has to be translated, and
# every msgstr in it is already empty.
pot_body "$POT" "$([ "$lang" = "en" ] && echo 1 || echo 0)"
} >"$po"
note "wrote po/$lang.po"
if [ "$lang" != "en" ]; then
note "next:"
note " translate it, then: scripts/i18n.sh check && make translations"
note " offer it in the options window, or nobody can pick it:"
note " src/optionsdialog.cpp: kLanguageCodes[] and languageOptions[]"
fi
}
usage() {
cat <<'EOF'
scripts/i18n.sh <command> [arguments]
extract regenerate po/gkeymapper.pot from the sources
update [lang...] merge the pot into the catalogs: new strings come in
untranslated, existing translations are kept
check validate the pot, the catalogs and the language list
status per-language overview (counts + compiled catalog)
todo <lang> what is still untranslated or fuzzy, with the source
locations from the pot
new <lang> [name] create po/<lang>.po from the pot (name = Language-Team,
e.g. "Italiano"); "new en" writes the identity catalog
help this text
The rules behind the commands are in po/README.md. gettext tools are used from
PATH; override them like the Makefile's variables, e.g. XGETTEXT=/usr/bin/xgettext
EOF
}
command="${1:-help}"
shift || true
case "$command" in
extract|pot) cmd_extract "$@" ;;
update) cmd_update "$@" ;;
check) cmd_check "$@" ;;
status) cmd_status "$@" ;;
todo) cmd_todo "$@" ;;
new) cmd_new "$@" ;;
help|-h|--help) usage ;;
*) usage; exit 1 ;;
esac

View File

@@ -24,6 +24,16 @@ inline constexpr const char* kBusName = "dev.rager.gkeymapper.Daemon";
inline constexpr const char* kObjectPath = "/dev/rager/gkeymapper/Daemon"; inline constexpr const char* kObjectPath = "/dev/rager/gkeymapper/Daemon";
inline constexpr const char* kInterface = "dev.rager.gkeymapper.Daemon"; inline constexpr const char* kInterface = "dev.rager.gkeymapper.Daemon";
// Where the GUI answers a second start (see src/singleinstance and src/main.cpp):
// it hands out the rows its window shows, so an instance that is replaced does not
// take unsaved edits with it. The name is the app id itself - GtkApplication owns
// that (see kBusName above), and the GUI is not a service, it only answers while a
// window is up.
inline constexpr const char* kWindowObjectPath = "/dev/rager/gkeymapper/Window";
inline constexpr const char* kWindowInterface = "dev.rager.gkeymapper.Window";
// The one method: no arguments, the rows as the mappings text (the file format).
inline constexpr const char* kWindowRowsMethod = "GetRows";
} // namespace gkeymapper::ipc } // namespace gkeymapper::ipc
#endif // GKEYMAPPER_IPC_IDS_H #endif // GKEYMAPPER_IPC_IDS_H

View File

@@ -95,8 +95,10 @@ bool set_autostart_enabled(bool enabled) {
// About dialog, so no translator has to carry a second sentence for it. // About dialog, so no translator has to carry a second sentence for it.
g_key_file_set_string(file, kGroup, "Comment", g_key_file_set_string(file, kGroup, "Comment",
_("G-Key mapper for the Logitech G910 (GTK4).")); _("G-Key mapper for the Logitech G910 (GTK4)."));
// Quoted: a path may contain a space. // Quoted: a path may contain a space. The flag keeps the window out of the way
g_autofree gchar* command = g_strdup_printf("\"%s\"", program_path().c_str()); // at login, see kStartHiddenArg.
g_autofree gchar* command =
g_strdup_printf("\"%s\" %s", program_path().c_str(), kStartHiddenArg);
g_key_file_set_string(file, kGroup, "Exec", command); g_key_file_set_string(file, kGroup, "Exec", command);
g_key_file_set_string(file, kGroup, "Icon", "gkeymapper"); g_key_file_set_string(file, kGroup, "Icon", "gkeymapper");
g_key_file_set_boolean(file, kGroup, "Terminal", FALSE); g_key_file_set_boolean(file, kGroup, "Terminal", FALSE);

View File

@@ -11,6 +11,13 @@ namespace gkeymapper {
// else - no flag in gui.conf that could disagree with it. // else - no flag in gui.conf that could disagree with it.
std::string autostart_file_path(); std::string autostart_file_path();
// The argument the entry hands to the program: the window is then built but stays
// in the tray (main.cpp), so a login does not put a window in front of the user,
// and asking for it - the tray's Show, or starting gKeyMapper by hand - brings it
// up. It belongs on the Exec line and not in gui.conf: the entry is what the
// session runs, so the flag is the whole truth about what happens at login.
inline constexpr const char* kStartHiddenArg = "--start-hidden";
// True when that file exists and starts the program. An entry the desktop's own // True when that file exists and starts the program. An entry the desktop's own
// tool has switched off (Hidden=true, or X-GNOME-Autostart-enabled=false) counts // tool has switched off (Hidden=true, or X-GNOME-Autostart-enabled=false) counts
// as off as well, so the switch shows what really happens at login. // as off as well, so the switch shows what really happens at login.

View File

@@ -47,13 +47,13 @@ const char* kParagraphs[] = {
N_("<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is running, which " N_("<b>Daemon status</b> – the footer shows whether gkeymapper-daemon is running, which "
"keyboard it found and the last G key that fired. <i>Options</i> shows the same in " "keyboard it found and the last G key that fired. <i>Options</i> shows the same in "
"more detail."), "more detail."),
N_("<b>Autostart</b> – <i>Start with the session</i> only opens this window when you log " N_("<b>Autostart</b> – <i>Start with the session</i> opens this window minimized to the tray "
"in. The G keys work without it: gkeymapper-daemon (a user service) reads them and " "when you log in. The G keys work without it: gkeymapper-daemon (a user service) reads "
"replays the actions, and that daemon is the only part that has to run."), "them and replays the actions, and that daemon is the only part that has to run."),
N_("<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden instead of " N_("<b>Tray and window</b> – with <i>Minimize to tray</i> the window is hidden instead of "
"closed when you close or minimize it; the tray icon shows it again (<i>Show</i>) and " "closed when you close or minimize it; the tray icon shows it again (<i>Show</i>) and "
"quits the program (<i>Close</i>). Starting gKeyMapper again closes the old window – " "quits the program (<i>Close</i>). Starting gKeyMapper again closes the old window – "
"rows that were not saved are lost with it."), "rows that were not saved yet come along."),
}; };
} // namespace } // namespace

View File

@@ -18,6 +18,7 @@
#include "mapping.h" #include "mapping.h"
#include "mappings.h" #include "mappings.h"
#include "optionsdialog.h" #include "optionsdialog.h"
#include "popovers.h"
#include "settings.h" #include "settings.h"
#include "singleinstance.h" #include "singleinstance.h"
#include "trayicon.h" #include "trayicon.h"
@@ -37,6 +38,18 @@ using gkeymapper::kNumKeys;
std::vector<KeyRow> rows; std::vector<KeyRow> rows;
// The rows of the instance this start replaced, as a mappings text (empty when
// there was none, or when it did not hand them over - an older build). main()
// fills this in before the window exists; build_ui() starts from it instead of
// from the file, so edits that were never saved survive a second start.
std::string handedOverRows;
// Set by main() when the session's autostart entry started this instance (the
// --start-hidden argument): the window is built but stays in the tray, so a login
// does not put it in the way. on_activate is what reads it - a window that is
// built later (a language switch) comes up the way it was left.
bool startHidden = false;
// The daemon view behind the footer. It is filled by the asynchronous callbacks // The daemon view behind the footer. It is filled by the asynchronous callbacks
// of DaemonClient, which arrive on the GTK main loop, so nothing here ever waits // of DaemonClient, which arrive on the GTK main loop, so nothing here ever waits
// for the daemon; the options window shows the same data in full. // for the daemon; the options window shows the same data in full.
@@ -373,6 +386,10 @@ static std::string find_asset(const std::string& name) {
// Size of the pixmap handed to the panel; the panel scales it to its own size. // Size of the pixmap handed to the panel; the panel scales it to its own size.
constexpr int kTrayIconSize = 64; constexpr int kTrayIconSize = 64;
// How long a start from the autostart entry waits for the tray icon before it
// gives up and shows the window (on_start_hidden_timeout).
constexpr guint kStartHiddenGraceMs = 5000;
// Shown and hidden are the two states the tray works with: a hidden window is // Shown and hidden are the two states the tray works with: a hidden window is
// still a window, so the app keeps running and the tray stays the way back. // still a window, so the app keeps running and the tray stays the way back.
static void show_main_window() { static void show_main_window() {
@@ -421,6 +438,19 @@ static gboolean on_minimize_watch(gpointer /*userData*/) {
return G_SOURCE_CONTINUE; return G_SOURCE_CONTINUE;
} }
// A start from the session's autostart entry keeps the window out of the way, but
// only a hosted tray icon can bring it back - a desktop without one would leave the
// program with no window and no way to open one. Nothing knows that at startup
// (the panel answers asynchronously), so the window is built hidden and appears
// here if the icon did not turn up in the meantime.
static gboolean on_start_hidden_timeout(gpointer /*userData*/) {
if (!tray.ready()) {
g_printerr("gkeymapper: no tray icon, showing the window\n");
show_main_window();
}
return G_SOURCE_REMOVE;
}
// The panel is another process and cannot read the icon compiled into this // The panel is another process and cannot read the icon compiled into this
// binary, so the tray icon travels as a pixmap. GdkPixbuf hands out RGBA, the // binary, so the tray icon travels as a pixmap. GdkPixbuf hands out RGBA, the
// protocol wants ARGB32 in network byte order, so the bytes are reordered here. // protocol wants ARGB32 in network byte order, so the bytes are reordered here.
@@ -478,7 +508,7 @@ static void start_tray(GtkApplication* app) {
[app]() { g_application_quit(G_APPLICATION(app)); }); [app]() { g_application_quit(G_APPLICATION(app)); });
} }
static void build_ui(GtkApplication* app); static void build_ui(GtkApplication* app, bool show);
static void open_options_dialog(); static void open_options_dialog();
// Rebuilt from scratch when the language changes: the widgets keep the strings // Rebuilt from scratch when the language changes: the widgets keep the strings
@@ -493,6 +523,9 @@ static gboolean rebuild_ui_idle(gpointer /*userData*/) {
// were open, so nothing on screen is left in the old language. // were open, so nothing on screen is left in the old language.
const bool helpOpen = gkeymapper::help_window_open(); const bool helpOpen = gkeymapper::help_window_open();
const bool optionsOpen = optionsDialog.is_open(); const bool optionsOpen = optionsDialog.is_open();
// A window that was in the tray (or was built hidden at login) stays there: the
// new one comes up the way the old one was, not in front of the user.
const bool windowShown = gtk_widget_get_visible(mainWindow);
// Rows that were not saved yet are widget state, not file content; without // Rows that were not saved yet are widget state, not file content; without
// this the rebuilt window would silently show the last saved file again. // this the rebuilt window would silently show the last saved file again.
const gkeymapper::MappingSet edits = gkeymapper::collect_mappings(rows); const gkeymapper::MappingSet edits = gkeymapper::collect_mappings(rows);
@@ -508,7 +541,7 @@ static gboolean rebuild_ui_idle(gpointer /*userData*/) {
gtk_window_destroy(GTK_WINDOW(mainWindow)); gtk_window_destroy(GTK_WINDOW(mainWindow));
mainWindow = nullptr; mainWindow = nullptr;
build_ui(app); build_ui(app, windowShown);
gkeymapper::apply_mappings(edits, rows); gkeymapper::apply_mappings(edits, rows);
if (helpOpen) { if (helpOpen) {
gkeymapper::show_help_window(GTK_WINDOW(mainWindow)); gkeymapper::show_help_window(GTK_WINDOW(mainWindow));
@@ -570,7 +603,7 @@ static void action_options(GSimpleAction* /*action*/, GVariant* /*parameter*/,
open_options_dialog(); open_options_dialog();
} }
void build_ui(GtkApplication* app) { void build_ui(GtkApplication* app, bool show) {
GtkWidget* window = gtk_application_window_new(app); GtkWidget* window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(window), _("gKeyMapper – Logitech G910")); gtk_window_set_title(GTK_WINDOW(window), _("gKeyMapper – Logitech G910"));
gtk_window_set_resizable(GTK_WINDOW(window), FALSE); gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
@@ -650,8 +683,16 @@ void build_ui(GtkApplication* app) {
gtk_grid_attach(GTK_GRID(grid), h, c, 0, 1, 1); gtk_grid_attach(GTK_GRID(grid), h, c, 0, 1, 1);
} }
// GTK draws the check mark of the selected entry right behind the text of a
// dropdown row, so every entry carries a trailing space. The catalogs keep
// the bare words; the space is added here, where the model is built.
const char* typeOptions[] = { _("Key"), _("Command"), _("Disabled"), const char* typeOptions[] = { _("Key"), _("Command"), _("Disabled"),
_("Key (hold)"), nullptr }; _("Key (hold)"), nullptr };
g_autoptr(GtkStringList) typeModel = gtk_string_list_new(nullptr);
for (int i = 0; typeOptions[i] != nullptr; ++i) {
g_autofree gchar* shown = g_strconcat(typeOptions[i], " ", nullptr);
gtk_string_list_append(typeModel, shown);
}
rows.resize(kNumKeys); rows.resize(kNumKeys);
for (int i = 0; i < kNumKeys; ++i) { for (int i = 0; i < kNumKeys; ++i) {
@@ -665,7 +706,7 @@ void build_ui(GtkApplication* app) {
rows[i].keyLabel = keyLabel; rows[i].keyLabel = keyLabel;
// Type dropdown (fixed width so rows don't jump between languages) // Type dropdown (fixed width so rows don't jump between languages)
GtkWidget* drop = gtk_drop_down_new_from_strings(typeOptions); GtkWidget* drop = gtk_drop_down_new(G_LIST_MODEL(typeModel), nullptr);
gtk_drop_down_set_selected(GTK_DROP_DOWN(drop), 0); gtk_drop_down_set_selected(GTK_DROP_DOWN(drop), 0);
gtk_widget_set_size_request(drop, 150, -1); gtk_widget_set_size_request(drop, 150, -1);
gtk_grid_attach(GTK_GRID(grid), drop, 1, i + 1, 1, 1); gtk_grid_attach(GTK_GRID(grid), drop, 1, i + 1, 1, 1);
@@ -691,13 +732,20 @@ void build_ui(GtkApplication* app) {
update_row_state(&rows[i]); update_row_state(&rows[i]);
} }
// Start from the saved mappings. Without this the rows are empty and the // Start from the rows the replaced instance had on screen, or - when this is
// first "Save mappings" of a freshly opened window would overwrite the // the only instance - from the saved mappings. Without either the rows are
// user's file with empty rows. A missing file is not an error (first start). // empty and the first "Save mappings" of a freshly opened window would
// overwrite the user's file with empty rows; a missing file is not an error
// (first start). Taken once: after this the live rows are the source.
if (!handedOverRows.empty()) {
gkeymapper::apply_mappings_text(handedOverRows, rows);
handedOverRows.clear();
} else {
const std::string configPath = gkeymapper::default_config_path(); const std::string configPath = gkeymapper::default_config_path();
if (g_file_test(configPath.c_str(), G_FILE_TEST_EXISTS)) { if (g_file_test(configPath.c_str(), G_FILE_TEST_EXISTS)) {
gkeymapper::read_mappings(configPath, rows); gkeymapper::read_mappings(configPath, rows);
} }
}
// Footer with save button and the daemon status line // Footer with save button and the daemon status line
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
@@ -735,11 +783,19 @@ void build_ui(GtkApplication* app) {
gtk_box_append(GTK_BOX(footer), daemonLabel); gtk_box_append(GTK_BOX(footer), daemonLabel);
mainWindow = window; mainWindow = window;
// The type dropdowns and the header bar menu are popovers; without this they
// grab the pointer and freeze the rest of the desktop while they are open.
gkeymapper::make_popovers_release_input(GTK_WINDOW(window));
// Closing is a request, not the end: the handler decides whether the window // Closing is a request, not the end: the handler decides whether the window
// hides to the tray or the app quits with it. // hides to the tray or the app quits with it.
g_signal_connect(window, "close-request", G_CALLBACK(on_close_request), nullptr); g_signal_connect(window, "close-request", G_CALLBACK(on_close_request), nullptr);
// `show` is false for the one start that must not put a window in the way: the
// one the session's autostart entry makes (on_start_hidden_timeout is what then
// decides whether it has to appear after all).
if (show) {
gtk_window_present(GTK_WINDOW(window)); gtk_window_present(GTK_WINDOW(window));
} }
}
static void on_activate(GtkApplication* app, gpointer /*user_data*/) { static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
// One window, always. A second "activate" (a desktop launch, or D-Bus // One window, always. A second "activate" (a desktop launch, or D-Bus
@@ -780,12 +836,27 @@ static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
const char* quitAccels[] = {"<Control>q", NULL}; const char* quitAccels[] = {"<Control>q", NULL};
gtk_application_set_accels_for_action(app, "app.quit", quitAccels); gtk_application_set_accels_for_action(app, "app.quit", quitAccels);
build_ui(app); // A start from the session leaves the window in the tray; a start by hand shows
// it (main() reads the flag off the command line).
build_ui(app, !startHidden);
// From here on this instance answers a new start with the rows its window
// shows, so the instance that replaces it does not lose edits that were never
// saved (see replace_running_instance). It has to come after build_ui(), which
// is what fills the rows.
gkeymapper::serve_rows_handover([]() { return gkeymapper::mappings_text(rows); });
// One-time, and deliberately not part of build_ui(): the window is rebuilt // One-time, and deliberately not part of build_ui(): the window is rebuilt
// when the language changes, while the tray, its poll and the daemon client // when the language changes, while the tray, its poll and the daemon client
// keep running as they are. // keep running as they are.
start_tray(app); start_tray(app);
if (startHidden) {
// The tray is what keeps a hidden window reachable; is it one? Only the
// panel can answer that, and it does so asynchronously (start_tray above is
// the question). Until then the window stays hidden and comes up if that
// question was never answered.
g_timeout_add(kStartHiddenGraceMs, on_start_hidden_timeout, nullptr);
}
// Four checks per second: one cheap call into the backend each, quick enough // Four checks per second: one cheap call into the backend each, quick enough
// that a minimized window does not linger in the task list. It does nothing // that a minimized window does not linger in the task list. It does nothing
// while the tray option is off. // while the tray option is off.
@@ -813,16 +884,32 @@ int main(int argc, char** argv) {
// the first translated string is built. // the first translated string is built.
gkeymapper::apply_gui_language(guiSettings.language); gkeymapper::apply_gui_language(guiSettings.language);
// The autostart entry starts the window in the tray and says so with an argument
// of its own (kStartHiddenArg). GTK does not know that argument and would stop
// on it, so it is taken out of the command line here, before it sees it.
std::vector<char*> args;
args.reserve(static_cast<size_t>(argc) + 1);
for (int i = 0; i < argc; ++i) {
if (g_strcmp0(argv[i], gkeymapper::kStartHiddenArg) == 0) {
startHidden = true;
continue;
}
args.push_back(argv[i]);
}
args.push_back(nullptr);
// One GUI only, and it is the one that was just started: close the instance // One GUI only, and it is the one that was just started: close the instance
// that already owns the app id (an older build, or the same one) before // that already owns the app id (an older build, or the same one) before
// becoming the primary instance. Without this the new process just hands over // becoming the primary instance. Without this the new process just hands over
// to the old one and its stale window stays on screen. // to the old one and its stale window stays on screen. Its rows come along,
gkeymapper::replace_running_instance(gkeymapper::ipc::kAppId); // so a mapping that was typed but not saved is not lost with it.
gkeymapper::replace_running_instance(gkeymapper::ipc::kAppId, &handedOverRows);
GtkApplication* app = gtk_application_new(gkeymapper::ipc::kAppId, GtkApplication* app = gtk_application_new(gkeymapper::ipc::kAppId,
G_APPLICATION_FLAGS_NONE); G_APPLICATION_FLAGS_NONE);
g_signal_connect(app, "activate", G_CALLBACK(on_activate), nullptr); g_signal_connect(app, "activate", G_CALLBACK(on_activate), nullptr);
int status = g_application_run(G_APPLICATION(app), argc, argv); int status = g_application_run(G_APPLICATION(app), static_cast<int>(args.size()) - 1,
args.data());
g_object_unref(app); g_object_unref(app);
return status; return status;
} }

View File

@@ -63,4 +63,12 @@ void apply_mappings(const MappingSet& set, const std::vector<KeyRow>& rows) {
} }
} }
std::string mappings_text(const std::vector<KeyRow>& rows) {
return format_mappings(collect_mappings(rows));
}
void apply_mappings_text(const std::string& text, const std::vector<KeyRow>& rows) {
apply_mappings(parse_mappings(text), rows);
}
} // namespace gkeymapper } // namespace gkeymapper

View File

@@ -17,6 +17,13 @@ namespace gkeymapper {
MappingSet collect_mappings(const std::vector<KeyRow>& rows); MappingSet collect_mappings(const std::vector<KeyRow>& rows);
void apply_mappings(const MappingSet& set, const std::vector<KeyRow>& rows); void apply_mappings(const MappingSet& set, const std::vector<KeyRow>& rows);
// The same round trip as text (the mappings file format), for rows that have to
// leave the process: the window that replaces a running instance is given them by
// it (see src/singleinstance), so edits that were never saved survive a second
// start as well.
std::string mappings_text(const std::vector<KeyRow>& rows);
void apply_mappings_text(const std::string& text, const std::vector<KeyRow>& rows);
// Collects the current widget state and writes it to path. // Collects the current widget state and writes it to path.
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows); bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows);

View File

@@ -10,6 +10,8 @@
#include <glib/gi18n.h> #include <glib/gi18n.h>
#include "popovers.h"
namespace gkeymapper { namespace gkeymapper {
namespace { namespace {
@@ -119,19 +121,30 @@ void OptionsDialog::show(GtkWindow* parent, const OptionsState& state, ToggleFn
gtk_box_append(GTK_BOX(box), autostartRow); gtk_box_append(GTK_BOX(box), autostartRow);
// Static text, so unlike the tray hint it needs no member: the switch is // Static text, so unlike the tray hint it needs no member: the switch is
// what changes. // what changes.
GtkWidget* autostartHint = hint_label(_("Opens this window when you log in. Not needed " GtkWidget* autostartHint = hint_label(_("Opens this window minimized to the tray when you "
"for the G keys - the daemon does the work and " "log in. Not needed for the G keys - the daemon "
"is the only part that has to run.")); "does the work and is the only part that has to "
"run."));
gtk_box_append(GTK_BOX(box), autostartHint); gtk_box_append(GTK_BOX(box), autostartHint);
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(box), section_label(_("Language"))); gtk_box_append(GTK_BOX(box), section_label(_("Language")));
// GtkDropDown has no label of its own; the heading above it says what it // GtkDropDown has no label of its own; the heading above it says what it
// is, and the accessible name says it again for screen readers. // is, and the accessible name says it again for screen readers.
// Trailing space like the type dropdown in the main window: GTK draws the
// check mark of the selected entry right behind the text.
const char* languageOptions[] = { _("System language"), "Deutsch", "English", "Español", const char* languageOptions[] = { _("System language"), "Deutsch", "English", "Español",
"Français", nullptr }; "Français", nullptr };
languageDrop_ = gtk_drop_down_new_from_strings(languageOptions); g_autoptr(GtkStringList) languageModel = gtk_string_list_new(nullptr);
gtk_widget_set_halign(languageDrop_, GTK_ALIGN_START); for (int i = 0; languageOptions[i] != nullptr; ++i) {
g_autofree gchar* shown = g_strconcat(languageOptions[i], " ", nullptr);
gtk_string_list_append(languageModel, shown);
}
languageDrop_ = gtk_drop_down_new(G_LIST_MODEL(languageModel), nullptr);
// Right-aligned like the switches above: the GUI's own controls sit on one
// line along the right edge, the text explains them on the left.
gtk_widget_set_halign(languageDrop_, GTK_ALIGN_END);
gtk_accessible_update_property(GTK_ACCESSIBLE(languageDrop_), gtk_accessible_update_property(GTK_ACCESSIBLE(languageDrop_),
GTK_ACCESSIBLE_PROPERTY_LABEL, _("Language"), -1); GTK_ACCESSIBLE_PROPERTY_LABEL, _("Language"), -1);
g_signal_connect(languageDrop_, "notify::selected", G_CALLBACK(on_language_changed), this); g_signal_connect(languageDrop_, "notify::selected", G_CALLBACK(on_language_changed), this);
@@ -168,6 +181,9 @@ void OptionsDialog::show(GtkWindow* parent, const OptionsState& state, ToggleFn
gtk_box_append(GTK_BOX(box), closeButton); gtk_box_append(GTK_BOX(box), closeButton);
g_signal_connect(window_, "destroy", G_CALLBACK(on_destroy), this); g_signal_connect(window_, "destroy", G_CALLBACK(on_destroy), this);
// The language dropdown is a popover; without this it grabs the pointer
// and freezes the rest of the desktop while it is open.
make_popovers_release_input(GTK_WINDOW(window_));
} }
apply(); apply();

144
src/popovers.cpp Normal file
View File

@@ -0,0 +1,144 @@
// gKeyMapper - popup handling that GTK gets wrong in this version.
//
// GTK 4.6's popovers - the list a dropdown opens and the header bar menu - grab
// the pointer while they are open. An X11 grab swallows every click of the
// other clients, and this GTK does not close the popup on a click outside the
// application either: with a dropdown open, nothing on the desktop can be
// clicked any more, neither another window nor the desktop itself, until Escape
// is pressed. GTK 3 replayed such a click to the window under the pointer, GTK 4
// dropped that. A popup that is left open is not the problem; a desktop that is
// frozen is.
//
// A popover only grabs while its "autohide" is on, so switching that off hands
// the clicks back to the rest of the desktop. The popups are then closed here,
// which is the part that autohide would have done:
// - a click in the window, outside the popup: on the press, so the click still
// reaches the widget under the pointer (a previous popup must not eat it),
// - a click on the widget that opened it: left alone, its own toggle closes it,
// - a click in the popup: left alone, it picks the entry and closes the popup,
// - a click anywhere else (another program, the desktop): the window loses the
// focus, and that is what closes it here - the click itself goes to whatever
// was clicked, which is the whole point,
// - Escape.
#include "popovers.h"
#include <vector>
namespace gkeymapper {
namespace {
void collect_popovers(GtkWidget* widget, std::vector<GtkWidget*>* popovers) {
for (GtkWidget* child = gtk_widget_get_first_child(widget); child != nullptr;
child = gtk_widget_get_next_sibling(child)) {
if (GTK_IS_POPOVER(child)) {
popovers->push_back(child);
}
collect_popovers(child, popovers);
}
}
void popovers_of(GtkWindow* window, std::vector<GtkWidget*>* popovers) {
collect_popovers(GTK_WIDGET(window), popovers);
}
// A press in the popup itself is a choice and must reach the entry under the
// pointer. GTK hands such a press to the popup's own surface, so this controller
// normally does not see it at all - the check is a guard for the case where it
// does, and it has to be by surface and not by position, because the coordinates
// of that press are the popup's and not the window's.
bool press_on_popup(GdkEvent* event, GtkWidget* popover) {
GdkSurface* popup = gtk_native_get_surface(GTK_NATIVE(popover));
return popup != nullptr && gdk_event_get_surface(event) == popup;
}
bool press_inside(GtkWidget* widget, GtkWindow* window, double x, double y) {
graphene_rect_t rect;
if (widget == nullptr || !gtk_widget_compute_bounds(widget, GTK_WIDGET(window), &rect)) {
return false;
}
return x >= rect.origin.x && x <= rect.origin.x + rect.size.width && y >= rect.origin.y &&
y <= rect.origin.y + rect.size.height;
}
void close_visible_popovers(GtkWindow* window) {
std::vector<GtkWidget*> popovers;
popovers_of(window, &popovers);
for (GtkWidget* popover : popovers) {
if (gtk_widget_get_visible(popover)) {
gtk_popover_popdown(GTK_POPOVER(popover));
}
}
}
gboolean on_event(GtkEventControllerLegacy* /*controller*/, GdkEvent* event, gpointer userData) {
GtkWindow* window = GTK_WINDOW(userData);
const GdkEventType type = gdk_event_get_event_type(event);
if (type != GDK_BUTTON_PRESS && type != GDK_KEY_PRESS) {
return GDK_EVENT_PROPAGATE;
}
std::vector<GtkWidget*> popovers;
popovers_of(window, &popovers);
for (GtkWidget* popover : popovers) {
// A popover GTK made after this was installed must not grab either; the
// press that opens it is seen here before the widget acts on it, and
// switching a visible one off drops the grab right away.
gtk_popover_set_autohide(GTK_POPOVER(popover), FALSE);
}
if (type == GDK_KEY_PRESS) {
if (g_strcmp0(gdk_keyval_name(gdk_key_event_get_keyval(event)), "Escape") == 0) {
for (GtkWidget* popover : popovers) {
if (gtk_widget_get_visible(popover)) {
gtk_popover_popdown(GTK_POPOVER(popover));
}
}
}
return GDK_EVENT_PROPAGATE;
}
double x = 0;
double y = 0;
gdk_event_get_position(event, &x, &y);
for (GtkWidget* popover : popovers) {
if (!gtk_widget_get_visible(popover) || press_on_popup(event, popover)) {
continue;
}
// The widget that opened the popup sits in the window, so the position is
// in the window's coordinates here.
if (press_inside(gtk_widget_get_parent(popover), window, x, y)) {
continue;
}
gtk_popover_popdown(GTK_POPOVER(popover));
}
return GDK_EVENT_PROPAGATE;
}
void on_focus_leave(GtkEventControllerFocus* /*controller*/, gpointer userData) {
close_visible_popovers(GTK_WINDOW(userData));
}
} // namespace
void make_popovers_release_input(GtkWindow* window) {
std::vector<GtkWidget*> popovers;
popovers_of(window, &popovers);
for (GtkWidget* popover : popovers) {
gtk_popover_set_autohide(GTK_POPOVER(popover), FALSE);
}
// A legacy controller does not claim the events it sees, so the widgets keep
// behaving as usual; the capture phase is what makes it see a press before
// the widget under the pointer does anything with it.
GtkEventController* events = gtk_event_controller_legacy_new();
gtk_event_controller_set_propagation_phase(events, GTK_PHASE_CAPTURE);
g_signal_connect(events, "event", G_CALLBACK(on_event), window);
gtk_widget_add_controller(GTK_WIDGET(window), events);
GtkEventController* focus = gtk_event_controller_focus_new();
g_signal_connect(focus, "leave", G_CALLBACK(on_focus_leave), window);
gtk_widget_add_controller(GTK_WIDGET(window), focus);
}
} // namespace gkeymapper

18
src/popovers.h Normal file
View File

@@ -0,0 +1,18 @@
// gKeyMapper - popup handling that GTK gets wrong in this version.
//
// See popovers.cpp for why this exists; in short, a popover that keeps its
// input grab makes the rest of the desktop unclickable on X11.
#pragma once
#include <gtk/gtk.h>
namespace gkeymapper {
// Call once per toplevel window, after the widgets are built. Every popover in
// the window (the dropdowns' popup lists, the header bar menu) then lets the
// pointer go, and is closed by us instead - outside click, Escape, or the
// window losing the focus.
void make_popovers_release_input(GtkWindow* window);
} // namespace gkeymapper

View File

@@ -3,16 +3,20 @@
// GtkApplication already makes a second process hand over to the first one (the // GtkApplication already makes a second process hand over to the first one (the
// app id is unique), but that leaves the *old* window on screen - including one // app id is unique), but that leaves the *old* window on screen - including one
// from an earlier build. So main() first closes what is running and only then // from an earlier build. So main() first closes what is running and only then
// becomes the primary instance itself. // becomes the primary instance itself. What it takes along are the rows that
// window shows: the takeover would otherwise drop edits that were never saved.
#include "singleinstance.h" #include "singleinstance.h"
#include <csignal> #include <csignal>
#include <utility>
#include <gio/gio.h> #include <gio/gio.h>
#include <glib.h> #include <glib.h>
#include <unistd.h> #include <unistd.h>
#include "ipc_ids.h"
namespace gkeymapper { namespace gkeymapper {
namespace { namespace {
@@ -22,6 +26,9 @@ constexpr int kQuitWaitMs = 1500;
constexpr int kTermWaitMs = 1500; constexpr int kTermWaitMs = 1500;
constexpr int kPollMs = 50; constexpr int kPollMs = 50;
// Asked by the next start (see serve_rows_handover).
RowsProvider gRowsProvider;
GDBusConnection* session_bus() { GDBusConnection* session_bus() {
GError* error = nullptr; GError* error = nullptr;
GDBusConnection* bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); GDBusConnection* bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
@@ -94,6 +101,33 @@ bool ask_to_quit(GDBusConnection* bus, const std::string& name) {
return true; return true;
} }
// The rows of the instance that is running, fetched before it goes away. No answer
// (an older build has no such method) simply leaves the new window with the file.
bool ask_for_rows(GDBusConnection* bus, const std::string& name, std::string& out) {
GVariant* reply = call(bus, name.c_str(), ipc::kWindowObjectPath, ipc::kWindowInterface,
ipc::kWindowRowsMethod, nullptr, G_VARIANT_TYPE("(s)"));
if (reply == nullptr) {
return false;
}
const gchar* text = nullptr;
g_variant_get(reply, "(&s)", &text);
out = text != nullptr ? text : "";
g_variant_unref(reply);
return true;
}
// Answers the next start. This runs on the main loop, which is the thread that
// owns the widgets, so the provider may read them.
void on_get_rows(GDBusConnection* /*connection*/, const gchar* /*sender*/,
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
const gchar* /*methodName*/, GVariant* /*parameters*/,
GDBusMethodInvocation* invocation, gpointer /*userData*/) {
const std::string rows = gRowsProvider ? gRowsProvider() : std::string();
g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", rows.c_str()));
}
const GDBusInterfaceVTable kHandoverVTable = {on_get_rows, nullptr, nullptr, {}};
pid_t owner_pid(GDBusConnection* bus, const std::string& name) { pid_t owner_pid(GDBusConnection* bus, const std::string& name) {
GVariant* reply = call(bus, "org.freedesktop.DBus", "/org/freedesktop/DBus", GVariant* reply = call(bus, "org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", "GetConnectionUnixProcessID", "org.freedesktop.DBus", "GetConnectionUnixProcessID",
@@ -124,7 +158,32 @@ bool is_our_gui(pid_t pid) {
} // namespace } // namespace
bool replace_running_instance(const std::string& appId) { void serve_rows_handover(RowsProvider provider) {
gRowsProvider = std::move(provider);
GDBusConnection* bus = session_bus();
if (bus == nullptr) {
return; // without a bus nobody can ask
}
// Built from the ids, so the XML and the constants cannot drift apart.
g_autofree gchar* xml = g_strdup_printf(
"<node><interface name='%s'><method name='%s'>"
"<arg type='s' name='rows' direction='out'/></method></interface></node>",
ipc::kWindowInterface, ipc::kWindowRowsMethod);
GError* error = nullptr;
GDBusNodeInfo* info = g_dbus_node_info_new_for_xml(xml, &error);
if (info != nullptr) {
// The registration lives as long as the process; being called twice would
// only report "already registered", which is not worth a message.
g_dbus_connection_register_object(bus, ipc::kWindowObjectPath, info->interfaces[0],
&kHandoverVTable, nullptr, nullptr, &error);
g_dbus_node_info_unref(info);
}
g_clear_error(&error);
g_object_unref(bus);
}
bool replace_running_instance(const std::string& appId, std::string* rowsOut) {
GDBusConnection* bus = session_bus(); GDBusConnection* bus = session_bus();
if (bus == nullptr) { if (bus == nullptr) {
return true; // without a bus there is no other instance either return true; // without a bus there is no other instance either
@@ -132,6 +191,11 @@ bool replace_running_instance(const std::string& appId) {
bool free = !has_owner(bus, appId); bool free = !has_owner(bus, appId);
if (!free) { if (!free) {
// The rows first, then the quit: after it there is nobody left to ask.
if (rowsOut != nullptr) {
ask_for_rows(bus, appId, *rowsOut);
}
// Clean path: the running instance quits itself. // Clean path: the running instance quits itself.
ask_to_quit(bus, appId); ask_to_quit(bus, appId);
free = wait_until_free(bus, appId, kQuitWaitMs); free = wait_until_free(bus, appId, kQuitWaitMs);

View File

@@ -2,6 +2,7 @@
#ifndef GKEYMAPPER_SINGLEINSTANCE_H #ifndef GKEYMAPPER_SINGLEINSTANCE_H
#define GKEYMAPPER_SINGLEINSTANCE_H #define GKEYMAPPER_SINGLEINSTANCE_H
#include <functional>
#include <string> #include <string>
namespace gkeymapper { namespace gkeymapper {
@@ -13,10 +14,25 @@ namespace gkeymapper {
// terminated with SIGTERM instead, after checking that the bus name really // terminated with SIGTERM instead, after checking that the bus name really
// belongs to a gkeymapper process. // belongs to a gkeymapper process.
// //
// Before the quit it asks that instance for the rows its window shows and puts
// them into `rowsOut` (the mappings text, the same format as the file). A new
// window starts from that instead of from the file, so edits that were never
// saved are not lost with the replaced window. An instance that does not answer
// (an older build) leaves `rowsOut` alone; pass nullptr to skip the asking.
//
// Returns true when the name is free afterwards, which is also the case when // Returns true when the name is free afterwards, which is also the case when
// nobody owned it. The caller still runs its usual single-instance logic, so a // nobody owned it. The caller still runs its usual single-instance logic, so a
// `false` (a hung instance that cannot be closed) only means "no new window". // `false` (a hung instance that cannot be closed) only means "no new window".
bool replace_running_instance(const std::string& appId); bool replace_running_instance(const std::string& appId, std::string* rowsOut = nullptr);
// The rows of this window, as `mappings_text` produces them.
using RowsProvider = std::function<std::string()>;
// The other side of the above: answers a new start that asks for the rows. Call it
// once the window exists; `provider` runs on this process's main loop, so reading
// the widgets is safe. Registering twice keeps the second provider, which only
// matters if a start could reach here more than once.
void serve_rows_handover(RowsProvider provider);
} // namespace gkeymapper } // namespace gkeymapper