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

View File

@@ -18,6 +18,7 @@
#include "mapping.h"
#include "mappings.h"
#include "optionsdialog.h"
#include "popovers.h"
#include "settings.h"
#include "singleinstance.h"
#include "trayicon.h"
@@ -37,6 +38,18 @@ using gkeymapper::kNumKeys;
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
// 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.
@@ -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.
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
// still a window, so the app keeps running and the tray stays the way back.
static void show_main_window() {
@@ -421,6 +438,19 @@ static gboolean on_minimize_watch(gpointer /*userData*/) {
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
// 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.
@@ -478,7 +508,7 @@ static void start_tray(GtkApplication* 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();
// 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.
const bool helpOpen = gkeymapper::help_window_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
// this the rebuilt window would silently show the last saved file again.
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));
mainWindow = nullptr;
build_ui(app);
build_ui(app, windowShown);
gkeymapper::apply_mappings(edits, rows);
if (helpOpen) {
gkeymapper::show_help_window(GTK_WINDOW(mainWindow));
@@ -570,7 +603,7 @@ static void action_options(GSimpleAction* /*action*/, GVariant* /*parameter*/,
open_options_dialog();
}
void build_ui(GtkApplication* app) {
void build_ui(GtkApplication* app, bool show) {
GtkWidget* window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(window), _("gKeyMapper – Logitech G910"));
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 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"),
_("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);
for (int i = 0; i < kNumKeys; ++i) {
@@ -665,7 +706,7 @@ void build_ui(GtkApplication* app) {
rows[i].keyLabel = keyLabel;
// 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_widget_set_size_request(drop, 150, -1);
gtk_grid_attach(GTK_GRID(grid), drop, 1, i + 1, 1, 1);
@@ -691,12 +732,19 @@ void build_ui(GtkApplication* app) {
update_row_state(&rows[i]);
}
// Start from the saved mappings. Without this the rows are 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).
const std::string configPath = gkeymapper::default_config_path();
if (g_file_test(configPath.c_str(), G_FILE_TEST_EXISTS)) {
gkeymapper::read_mappings(configPath, rows);
// Start from the rows the replaced instance had on screen, or - when this is
// the only instance - from the saved mappings. Without either the rows are
// 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();
if (g_file_test(configPath.c_str(), G_FILE_TEST_EXISTS)) {
gkeymapper::read_mappings(configPath, rows);
}
}
// Footer with save button and the daemon status line
@@ -735,10 +783,18 @@ void build_ui(GtkApplication* app) {
gtk_box_append(GTK_BOX(footer), daemonLabel);
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
// hides to the tray or the app quits with it.
g_signal_connect(window, "close-request", G_CALLBACK(on_close_request), nullptr);
gtk_window_present(GTK_WINDOW(window));
// `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));
}
}
static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
@@ -780,12 +836,27 @@ static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
const char* quitAccels[] = {"<Control>q", NULL};
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
// when the language changes, while the tray, its poll and the daemon client
// keep running as they are.
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
// that a minimized window does not linger in the task list. It does nothing
// while the tray option is off.
@@ -813,16 +884,32 @@ int main(int argc, char** argv) {
// the first translated string is built.
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
// 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
// to the old one and its stale window stays on screen.
gkeymapper::replace_running_instance(gkeymapper::ipc::kAppId);
// to the old one and its stale window stays on screen. Its rows come along,
// 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,
G_APPLICATION_FLAGS_NONE);
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);
return status;
}