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

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

View File

@@ -47,13 +47,13 @@ const char* kParagraphs[] = {
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 "
"more detail."),
N_("<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."),
N_("<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."),
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 "
"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

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;
}

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

View File

@@ -17,6 +17,13 @@ namespace gkeymapper {
MappingSet collect_mappings(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.
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows);

View File

@@ -10,6 +10,8 @@
#include <glib/gi18n.h>
#include "popovers.h"
namespace gkeymapper {
namespace {
@@ -119,19 +121,30 @@ void OptionsDialog::show(GtkWindow* parent, const OptionsState& state, ToggleFn
gtk_box_append(GTK_BOX(box), autostartRow);
// Static text, so unlike the tray hint it needs no member: the switch is
// what changes.
GtkWidget* autostartHint = hint_label(_("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."));
GtkWidget* autostartHint = hint_label(_("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."));
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), section_label(_("Language")));
// 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.
// 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",
"Français", nullptr };
languageDrop_ = gtk_drop_down_new_from_strings(languageOptions);
gtk_widget_set_halign(languageDrop_, GTK_ALIGN_START);
g_autoptr(GtkStringList) languageModel = gtk_string_list_new(nullptr);
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_PROPERTY_LABEL, _("Language"), -1);
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);
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();

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
// 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
// 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 <csignal>
#include <utility>
#include <gio/gio.h>
#include <glib.h>
#include <unistd.h>
#include "ipc_ids.h"
namespace gkeymapper {
namespace {
@@ -22,6 +26,9 @@ constexpr int kQuitWaitMs = 1500;
constexpr int kTermWaitMs = 1500;
constexpr int kPollMs = 50;
// Asked by the next start (see serve_rows_handover).
RowsProvider gRowsProvider;
GDBusConnection* session_bus() {
GError* error = nullptr;
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;
}
// 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) {
GVariant* reply = call(bus, "org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", "GetConnectionUnixProcessID",
@@ -124,7 +158,32 @@ bool is_our_gui(pid_t pid) {
} // 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();
if (bus == nullptr) {
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);
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.
ask_to_quit(bus, appId);
free = wait_until_free(bus, appId, kQuitWaitMs);

View File

@@ -2,6 +2,7 @@
#ifndef GKEYMAPPER_SINGLEINSTANCE_H
#define GKEYMAPPER_SINGLEINSTANCE_H
#include <functional>
#include <string>
namespace gkeymapper {
@@ -13,10 +14,25 @@ namespace gkeymapper {
// terminated with SIGTERM instead, after checking that the bus name really
// 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
// 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".
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