Fix Dropdown issue
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
117
src/main.cpp
117
src/main.cpp
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
144
src/popovers.cpp
Normal 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
18
src/popovers.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user