Fix Dropdown issue
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user