Autostart option added
This commit is contained in:
117
src/autostart.cpp
Normal file
117
src/autostart.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
// gKeyMapper - the autostart entry that opens the window at login.
|
||||
//
|
||||
// What this starts is the window, never the daemon: the daemon is the part that
|
||||
// has to run for the G keys to work, it is a systemd user service (see data/), and
|
||||
// a desktop file does not own it. The window is a convenience - the mappings can
|
||||
// be edited any time, and the G keys work with the window closed.
|
||||
|
||||
#include "autostart.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
const char* kGroup = "Desktop Entry";
|
||||
|
||||
std::string autostart_dir() {
|
||||
return std::string(g_get_user_config_dir()) + "/autostart";
|
||||
}
|
||||
|
||||
// The binary that is running now. A build in the working tree keeps working after
|
||||
// `make` because the path stays valid, and an installed one resolves to
|
||||
// /usr/local/bin/gkeymapper, so the entry always starts the program the user is
|
||||
// looking at. Only without /proc the name is left to PATH.
|
||||
std::string program_path() {
|
||||
GError* error = nullptr;
|
||||
g_autofree gchar* path = g_file_read_link("/proc/self/exe", &error);
|
||||
if (path != nullptr) {
|
||||
return path;
|
||||
}
|
||||
g_clear_error(&error);
|
||||
return "gkeymapper";
|
||||
}
|
||||
|
||||
// An entry can be there and still do nothing: the desktop's startup-applications
|
||||
// tool disables it instead of deleting it. Both spellings mean "off" here.
|
||||
bool disabled_by_desktop(GKeyFile* file) {
|
||||
GError* itemError = nullptr;
|
||||
const gboolean hidden = g_key_file_get_boolean(file, kGroup, "Hidden", &itemError);
|
||||
const bool isHidden = itemError == nullptr && hidden != FALSE;
|
||||
g_clear_error(&itemError);
|
||||
|
||||
const gboolean gnomeEnabled =
|
||||
g_key_file_get_boolean(file, kGroup, "X-GNOME-Autostart-enabled", &itemError);
|
||||
const bool isDisabled = itemError == nullptr && gnomeEnabled == FALSE;
|
||||
g_clear_error(&itemError);
|
||||
|
||||
return isHidden || isDisabled;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string autostart_file_path() {
|
||||
return autostart_dir() + "/gkeymapper.desktop";
|
||||
}
|
||||
|
||||
bool autostart_enabled() {
|
||||
GKeyFile* file = g_key_file_new();
|
||||
GError* error = nullptr;
|
||||
// A missing file is the normal state for "off", not an error.
|
||||
const gboolean loaded =
|
||||
g_key_file_load_from_file(file, autostart_file_path().c_str(), G_KEY_FILE_NONE, &error);
|
||||
g_clear_error(&error);
|
||||
|
||||
const bool enabled = loaded != FALSE && !disabled_by_desktop(file);
|
||||
g_key_file_unref(file);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool set_autostart_enabled(bool enabled) {
|
||||
const std::string path = autostart_file_path();
|
||||
|
||||
if (!enabled) {
|
||||
if (!g_file_test(path.c_str(), G_FILE_TEST_EXISTS)) {
|
||||
return true; // nothing to undo
|
||||
}
|
||||
if (g_remove(path.c_str()) != 0) {
|
||||
g_printerr("gkeymapper: could not remove %s\n", path.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
g_autofree gchar* directory = g_path_get_dirname(path.c_str());
|
||||
if (g_mkdir_with_parents(directory, 0700) != 0) {
|
||||
g_printerr("gkeymapper: could not create %s\n", directory);
|
||||
return false;
|
||||
}
|
||||
|
||||
GKeyFile* file = g_key_file_new();
|
||||
g_key_file_set_string(file, kGroup, "Type", "Application");
|
||||
g_key_file_set_string(file, kGroup, "Name", "gKeyMapper");
|
||||
// The desktop shows this next to the name; the wording is the one from the
|
||||
// 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());
|
||||
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);
|
||||
g_key_file_set_boolean(file, kGroup, "StartupNotify", FALSE);
|
||||
g_key_file_set_string(file, kGroup, "Categories", "Utility;");
|
||||
|
||||
GError* error = nullptr;
|
||||
const gboolean written = g_key_file_save_to_file(file, path.c_str(), &error);
|
||||
if (written == FALSE) {
|
||||
g_printerr("gkeymapper: could not write %s: %s\n", path.c_str(),
|
||||
error != nullptr ? error->message : "unknown error");
|
||||
g_clear_error(&error);
|
||||
}
|
||||
g_key_file_unref(file);
|
||||
return written != FALSE;
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
25
src/autostart.h
Normal file
25
src/autostart.h
Normal file
@@ -0,0 +1,25 @@
|
||||
// gKeyMapper - the autostart entry that opens the window at login.
|
||||
#ifndef GKEYMAPPER_AUTOSTART_H
|
||||
#define GKEYMAPPER_AUTOSTART_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// The session's autostart entry: ~/.config/autostart/gkeymapper.desktop. Every
|
||||
// desktop reads that directory at login, so the switch is one file and nothing
|
||||
// else - no flag in gui.conf that could disagree with it.
|
||||
std::string autostart_file_path();
|
||||
|
||||
// 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.
|
||||
bool autostart_enabled();
|
||||
|
||||
// Writes the entry (starting the binary that is running now) or removes it.
|
||||
// False when the file could not be written or removed.
|
||||
bool set_autostart_enabled(bool enabled);
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_AUTOSTART_H
|
||||
@@ -18,6 +18,18 @@ struct DaemonStatus {
|
||||
std::string configPath;
|
||||
};
|
||||
|
||||
// The daemon as the GUI shows it: the last status reply plus what the signals
|
||||
// added since (the last G key that fired). The footer shows a one-line summary of
|
||||
// it, the options window the full picture.
|
||||
struct DaemonView {
|
||||
bool reachable = false;
|
||||
bool configOk = true;
|
||||
std::string version;
|
||||
std::string device;
|
||||
std::string configPath;
|
||||
std::string lastKey;
|
||||
};
|
||||
|
||||
// Control-plane client for the daemon. Every call is asynchronous and its
|
||||
// result arrives on the GTK main loop, so the UI never blocks on the daemon
|
||||
// (a call is answered at most one daemon loop iteration late). Keypresses never
|
||||
|
||||
115
src/help.cpp
Normal file
115
src/help.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
// gKeyMapper - the help window.
|
||||
//
|
||||
// One page, no scrolling and no search: the program has nine rows and four
|
||||
// types, so the help is a handful of paragraphs. Each paragraph is one
|
||||
// translated string including its bold term, so a translator sees the sentence
|
||||
// the user sees.
|
||||
|
||||
#include "help.h"
|
||||
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
|
||||
GtkWidget* helpWindow = nullptr;
|
||||
|
||||
// One paragraph: Pango markup (the bold term), wrapped at a readable width.
|
||||
GtkWidget* paragraph(const char* text) {
|
||||
GtkWidget* label = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(label), text);
|
||||
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(label), 62);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
return label;
|
||||
}
|
||||
|
||||
void on_destroy(GtkWidget* /*widget*/, gpointer /*userData*/) {
|
||||
helpWindow = nullptr;
|
||||
}
|
||||
|
||||
const char* kParagraphs[] = {
|
||||
N_("<b>The nine rows</b> – one per G key. Choose the type, then enter or capture the "
|
||||
"target and press <i>Save mappings</i>."),
|
||||
N_("<b>Key</b> – presses the captured sequence once, e.g. ctrl+c."),
|
||||
N_("<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."),
|
||||
N_("<b>Command</b> – runs a program or a command line, e.g. /usr/bin/firefox --new-window."),
|
||||
N_("<b>Disabled</b> – the key does nothing."),
|
||||
N_("<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."),
|
||||
N_("<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."),
|
||||
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>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."),
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void show_help_window(GtkWindow* parent) {
|
||||
if (helpWindow != nullptr) {
|
||||
gtk_window_present(GTK_WINDOW(helpWindow));
|
||||
return;
|
||||
}
|
||||
GtkWidget* window = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(window), _("Help"));
|
||||
gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
|
||||
// Themed like the main window: same icon, same resource path (registered by
|
||||
// build_ui, which always runs before the menu can be used).
|
||||
gtk_window_set_icon_name(GTK_WINDOW(window), "gkeymapper");
|
||||
if (parent != nullptr) {
|
||||
gtk_window_set_transient_for(GTK_WINDOW(window), parent);
|
||||
}
|
||||
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_top(box, 18);
|
||||
gtk_widget_set_margin_bottom(box, 18);
|
||||
gtk_widget_set_margin_start(box, 18);
|
||||
gtk_widget_set_margin_end(box, 18);
|
||||
gtk_window_set_child(GTK_WINDOW(window), box);
|
||||
|
||||
GtkWidget* heading = gtk_label_new(nullptr);
|
||||
// The heading is the same word as the menu entry, only bigger; the markup is
|
||||
// added around the translation so no translator has to carry tags for it.
|
||||
g_autofree gchar* headingMarkup = g_strdup_printf("<big><b>%s</b></big>", _("Help"));
|
||||
gtk_label_set_markup(GTK_LABEL(heading), headingMarkup);
|
||||
gtk_label_set_xalign(GTK_LABEL(heading), 0.0f);
|
||||
gtk_box_append(GTK_BOX(box), heading);
|
||||
|
||||
for (const char* text : kParagraphs) {
|
||||
gtk_box_append(GTK_BOX(box), paragraph(_(text)));
|
||||
}
|
||||
|
||||
GtkWidget* closeButton = gtk_button_new_with_label(_("Close window"));
|
||||
gtk_widget_set_halign(closeButton, GTK_ALIGN_END);
|
||||
g_signal_connect_swapped(closeButton, "clicked", G_CALLBACK(gtk_window_destroy), window);
|
||||
gtk_box_append(GTK_BOX(box), closeButton);
|
||||
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), nullptr);
|
||||
helpWindow = window;
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
}
|
||||
|
||||
bool help_window_open() {
|
||||
return helpWindow != nullptr;
|
||||
}
|
||||
|
||||
void close_help_window() {
|
||||
if (helpWindow != nullptr) {
|
||||
gtk_window_destroy(GTK_WINDOW(helpWindow));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
21
src/help.h
Normal file
21
src/help.h
Normal file
@@ -0,0 +1,21 @@
|
||||
// gKeyMapper - the help window.
|
||||
#ifndef GKEYMAPPER_HELP_H
|
||||
#define GKEYMAPPER_HELP_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// Explains the window in one page: what the nine rows, the four types, the
|
||||
// capture dialog, the daemon status and the tray do. Opens the help window, or
|
||||
// brings the open one to the front.
|
||||
void show_help_window(GtkWindow* parent);
|
||||
|
||||
// True while the window is on screen. Its texts are translated when it is built,
|
||||
// so a language switch closes it (and reopens it if it was open).
|
||||
bool help_window_open();
|
||||
void close_help_window();
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_HELP_H
|
||||
323
src/main.cpp
323
src/main.cpp
@@ -9,13 +9,18 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "autostart.h"
|
||||
#include "capturedialog.h"
|
||||
#include "daemonclient.h"
|
||||
#include "help.h"
|
||||
#include "ipc_ids.h"
|
||||
#include "keyrow.h"
|
||||
#include "mapping.h"
|
||||
#include "mappings.h"
|
||||
#include "optionsdialog.h"
|
||||
#include "settings.h"
|
||||
#include "singleinstance.h"
|
||||
#include "trayicon.h"
|
||||
|
||||
#ifndef PACKAGE
|
||||
#define PACKAGE "gkeymapper"
|
||||
@@ -32,19 +37,21 @@ using gkeymapper::kNumKeys;
|
||||
|
||||
std::vector<KeyRow> rows;
|
||||
|
||||
// Footer view of the daemon. It is filled by the asynchronous callbacks of
|
||||
// DaemonClient, which arrive on the GTK main loop, so nothing here ever waits
|
||||
// for the daemon.
|
||||
struct DaemonFooter {
|
||||
bool reachable = false;
|
||||
bool configOk = true;
|
||||
std::string device;
|
||||
std::string lastKey;
|
||||
} daemonFooter;
|
||||
// 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.
|
||||
gkeymapper::DaemonView daemonView;
|
||||
|
||||
GtkWidget* daemonLabel = nullptr;
|
||||
gkeymapper::DaemonClient daemonClient;
|
||||
|
||||
// The window can be hidden instead of closed (the tray brings it back), and the
|
||||
// options window keeps showing the daemon status while it is open.
|
||||
GtkWidget* mainWindow = nullptr;
|
||||
gkeymapper::TrayIcon tray;
|
||||
gkeymapper::GuiSettings guiSettings;
|
||||
gkeymapper::OptionsDialog optionsDialog;
|
||||
|
||||
// Rebuilds the footer line from the state above. " · " is a separator, not a
|
||||
// sentence, so it is not translated.
|
||||
static void refresh_footer() {
|
||||
@@ -52,47 +59,53 @@ static void refresh_footer() {
|
||||
return;
|
||||
}
|
||||
std::string text;
|
||||
if (!daemonFooter.reachable) {
|
||||
if (!daemonView.reachable) {
|
||||
text = _("Daemon: not reachable");
|
||||
} else {
|
||||
text = _("Daemon: running");
|
||||
text += " · ";
|
||||
if (daemonFooter.device.empty()) {
|
||||
if (daemonView.device.empty()) {
|
||||
// Reachable but no keyboard: unplugged, unsupported, or not found
|
||||
// yet. Saying so is the difference between "broken" and "unplugged".
|
||||
text += _("Device: not found");
|
||||
} else {
|
||||
g_autofree gchar* device =
|
||||
g_strdup_printf(_("Device: %s"), daemonFooter.device.c_str());
|
||||
g_strdup_printf(_("Device: %s"), daemonView.device.c_str());
|
||||
text += device;
|
||||
}
|
||||
if (!daemonFooter.configOk) {
|
||||
if (!daemonView.configOk) {
|
||||
text += " · ";
|
||||
text += _("Configuration error");
|
||||
}
|
||||
if (!daemonFooter.lastKey.empty()) {
|
||||
if (!daemonView.lastKey.empty()) {
|
||||
text += " · ";
|
||||
text += daemonFooter.lastKey;
|
||||
g_autofree gchar* lastKey =
|
||||
g_strdup_printf(_("Last key: %s"), daemonView.lastKey.c_str());
|
||||
text += lastKey;
|
||||
}
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(daemonLabel), text.c_str());
|
||||
}
|
||||
|
||||
static void on_daemon_status(const gkeymapper::DaemonStatus& status) {
|
||||
daemonFooter.reachable = status.reachable;
|
||||
daemonFooter.configOk = status.configOk;
|
||||
daemonFooter.device = status.device;
|
||||
daemonView.reachable = status.reachable;
|
||||
daemonView.configOk = status.configOk;
|
||||
daemonView.version = status.version;
|
||||
daemonView.device = status.device;
|
||||
daemonView.configPath = status.configPath;
|
||||
if (!status.reachable) {
|
||||
daemonFooter.lastKey.clear(); // stale once the daemon is gone
|
||||
daemonView.lastKey.clear(); // stale once the daemon is gone
|
||||
}
|
||||
refresh_footer();
|
||||
optionsDialog.update(daemonView);
|
||||
}
|
||||
|
||||
static void on_daemon_key(int index, const std::string& action) {
|
||||
g_autofree gchar* text =
|
||||
g_strdup_printf(_("Last key: G%d → %s"), index + 1, action.c_str());
|
||||
daemonFooter.lastKey = text;
|
||||
// Just the fact ("G4 → key:ctrl+v"), no label: the footer and the options
|
||||
// window each put their own translated label in front of it.
|
||||
daemonView.lastKey = "G" + std::to_string(index + 1) + " → " + action;
|
||||
refresh_footer();
|
||||
optionsDialog.update(daemonView);
|
||||
}
|
||||
|
||||
static void on_pick_file_response(GtkDialog* dialog, int response, gpointer userData) {
|
||||
@@ -230,6 +243,22 @@ static GtkWidget* make_icon_image(int pixelSize) {
|
||||
return image;
|
||||
}
|
||||
|
||||
// GtkAboutDialog makes its texts selectable and hands the keyboard focus to the
|
||||
// program name (the first focusable widget), so the dialog opens with the name
|
||||
// drawn as selected — the blue block nobody asked for. Nothing in that dialog is
|
||||
// worth copying (the version is short, and the options window shows it too), so
|
||||
// the labels are switched off as selectable; that also stops the name from taking
|
||||
// the focus, and no other label claims it afterwards.
|
||||
static void make_labels_unselectable(GtkWidget* widget) {
|
||||
if (GTK_IS_LABEL(widget)) {
|
||||
gtk_label_set_selectable(GTK_LABEL(widget), FALSE);
|
||||
}
|
||||
for (GtkWidget* child = gtk_widget_get_first_child(widget); child != nullptr;
|
||||
child = gtk_widget_get_next_sibling(child)) {
|
||||
make_labels_unselectable(child);
|
||||
}
|
||||
}
|
||||
|
||||
static void show_about_dialog(GtkWindow* parent) {
|
||||
GtkWidget* dialog = gtk_about_dialog_new();
|
||||
gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(dialog), _("gKeyMapper"));
|
||||
@@ -256,6 +285,8 @@ static void show_about_dialog(GtkWindow* parent) {
|
||||
g_object_unref(texture);
|
||||
}
|
||||
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
|
||||
// Before it is mapped: that is when the focus would go to the program name.
|
||||
make_labels_unselectable(dialog);
|
||||
// No "response" handler: GtkAboutDialog has no such signal in GTK4 (only
|
||||
// GtkDialog subclasses with buttons, like GtkFileChooserDialog, do). Closing
|
||||
// it goes through close-request, whose default handler destroys the window.
|
||||
@@ -339,6 +370,206 @@ static std::string find_asset(const std::string& name) {
|
||||
return local;
|
||||
}
|
||||
|
||||
// Size of the pixmap handed to the panel; the panel scales it to its own size.
|
||||
constexpr int kTrayIconSize = 64;
|
||||
|
||||
// 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() {
|
||||
if (mainWindow == nullptr) {
|
||||
return;
|
||||
}
|
||||
gtk_widget_set_visible(mainWindow, TRUE);
|
||||
// present() also restores a window the window manager had minimized.
|
||||
gtk_window_present(GTK_WINDOW(mainWindow));
|
||||
}
|
||||
|
||||
static void hide_main_window() {
|
||||
if (mainWindow != nullptr) {
|
||||
gtk_widget_set_visible(mainWindow, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
// Hiding is only safe while the tray really hosts the icon: without one there
|
||||
// would be no way back to the window, so the app quits as usual.
|
||||
static bool hides_to_tray() {
|
||||
return guiSettings.minimizeToTray && tray.ready();
|
||||
}
|
||||
|
||||
// Closing the window: with the tray option on it is hidden, otherwise the default
|
||||
// handler closes the window and the app quits with it.
|
||||
static gboolean on_close_request(GtkWindow* /*window*/, gpointer /*userData*/) {
|
||||
if (!hides_to_tray()) {
|
||||
return FALSE;
|
||||
}
|
||||
hide_main_window();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// GTK4 has no signal for "minimized" (GdkToplevel has no properties either, only
|
||||
// gdk_toplevel_get_state), so the one state that needs an action is polled: one
|
||||
// call per tick, and only while the option is on and the window is up.
|
||||
static gboolean on_minimize_watch(gpointer /*userData*/) {
|
||||
if (!hides_to_tray() || mainWindow == nullptr || !gtk_widget_get_visible(mainWindow)) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
GdkSurface* surface = gtk_native_get_surface(GTK_NATIVE(mainWindow));
|
||||
if (surface != nullptr && GDK_IS_TOPLEVEL(surface) &&
|
||||
(gdk_toplevel_get_state(GDK_TOPLEVEL(surface)) & GDK_TOPLEVEL_STATE_MINIMIZED) != 0) {
|
||||
hide_main_window();
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
// 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.
|
||||
static bool load_tray_icon(std::vector<unsigned char>& pixels, int& width, int& height) {
|
||||
GError* error = nullptr;
|
||||
GdkPixbuf* pixbuf = have_embedded_icon()
|
||||
? gdk_pixbuf_new_from_resource(kIconResource, &error)
|
||||
: gdk_pixbuf_new_from_file(find_asset("icon.png").c_str(), &error);
|
||||
if (pixbuf == nullptr) {
|
||||
g_printerr("gkeymapper: no tray icon: %s\n",
|
||||
error != nullptr ? error->message : "unknown error");
|
||||
g_clear_error(&error);
|
||||
return false;
|
||||
}
|
||||
if (gdk_pixbuf_get_width(pixbuf) > kTrayIconSize ||
|
||||
gdk_pixbuf_get_height(pixbuf) > kTrayIconSize) {
|
||||
GdkPixbuf* scaled =
|
||||
gdk_pixbuf_scale_simple(pixbuf, kTrayIconSize, kTrayIconSize, GDK_INTERP_BILINEAR);
|
||||
g_object_unref(pixbuf);
|
||||
pixbuf = scaled;
|
||||
}
|
||||
if (!gdk_pixbuf_get_has_alpha(pixbuf)) {
|
||||
GdkPixbuf* withAlpha = gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0);
|
||||
g_object_unref(pixbuf);
|
||||
pixbuf = withAlpha;
|
||||
}
|
||||
width = gdk_pixbuf_get_width(pixbuf);
|
||||
height = gdk_pixbuf_get_height(pixbuf);
|
||||
const int stride = gdk_pixbuf_get_rowstride(pixbuf);
|
||||
const guchar* source = gdk_pixbuf_get_pixels(pixbuf);
|
||||
pixels.assign(static_cast<size_t>(width) * static_cast<size_t>(height) * 4, 0);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const guchar* row = source + static_cast<size_t>(y) * static_cast<size_t>(stride);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
unsigned char* target =
|
||||
pixels.data() + (static_cast<size_t>(y) * static_cast<size_t>(width) + x) * 4;
|
||||
target[0] = row[x * 4 + 3]; // A
|
||||
target[1] = row[x * 4 + 0]; // R
|
||||
target[2] = row[x * 4 + 1]; // G
|
||||
target[3] = row[x * 4 + 2]; // B
|
||||
}
|
||||
}
|
||||
g_object_unref(pixbuf);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void start_tray(GtkApplication* app) {
|
||||
std::vector<unsigned char> pixels;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (!load_tray_icon(pixels, width, height)) {
|
||||
return;
|
||||
}
|
||||
tray.start(std::move(pixels), width, height, show_main_window,
|
||||
[app]() { g_application_quit(G_APPLICATION(app)); });
|
||||
}
|
||||
|
||||
static void build_ui(GtkApplication* app);
|
||||
static void open_options_dialog();
|
||||
|
||||
// Rebuilt from scratch when the language changes: the widgets keep the strings
|
||||
// they were made with, so switching means building them again. Runs as an idle,
|
||||
// because the caller is the dropdown that this destroys.
|
||||
static gboolean rebuild_ui_idle(gpointer /*userData*/) {
|
||||
if (mainWindow == nullptr) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
GtkApplication* app = gtk_window_get_application(GTK_WINDOW(mainWindow));
|
||||
// Both are transient windows: they go with the rebuild and come back if they
|
||||
// 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();
|
||||
// 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);
|
||||
|
||||
gkeymapper::apply_gui_language(guiSettings.language);
|
||||
|
||||
// Destroying the last window would end the application, so it is held while
|
||||
// there is none.
|
||||
g_application_hold(G_APPLICATION(app));
|
||||
optionsDialog.close();
|
||||
gkeymapper::close_help_window();
|
||||
daemonLabel = nullptr; // dangling from here on, and a reply may still arrive
|
||||
gtk_window_destroy(GTK_WINDOW(mainWindow));
|
||||
mainWindow = nullptr;
|
||||
|
||||
build_ui(app);
|
||||
gkeymapper::apply_mappings(edits, rows);
|
||||
if (helpOpen) {
|
||||
gkeymapper::show_help_window(GTK_WINDOW(mainWindow));
|
||||
}
|
||||
if (optionsOpen) {
|
||||
open_options_dialog();
|
||||
}
|
||||
g_application_release(G_APPLICATION(app));
|
||||
|
||||
refresh_footer();
|
||||
// The panel mirrors the menu once, so the new labels have to be announced.
|
||||
tray.refresh_labels();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// Chosen in the options window. Only remembered here; the switch itself is what
|
||||
// the user sees next, and that is the rebuild.
|
||||
static void on_language_selected(const std::string& language) {
|
||||
if (language == guiSettings.language) {
|
||||
return;
|
||||
}
|
||||
guiSettings.language = language;
|
||||
gkeymapper::save_gui_settings(guiSettings);
|
||||
g_idle_add(rebuild_ui_idle, nullptr);
|
||||
}
|
||||
|
||||
static void open_options_dialog() {
|
||||
// None of the switches needs a restart: the tray one decides what closing
|
||||
// does next, autostart only writes or removes the session's entry, and the
|
||||
// language one rebuilds the windows right away.
|
||||
gkeymapper::OptionsState state;
|
||||
state.trayAvailable = tray.ready();
|
||||
state.autostart = gkeymapper::autostart_enabled();
|
||||
state.settings = guiSettings;
|
||||
state.view = daemonView;
|
||||
optionsDialog.show(mainWindow != nullptr ? GTK_WINDOW(mainWindow) : nullptr, state,
|
||||
[](bool minimizeToTray) {
|
||||
guiSettings.minimizeToTray = minimizeToTray;
|
||||
gkeymapper::save_gui_settings(guiSettings);
|
||||
},
|
||||
[](bool autostart) {
|
||||
// Starting the window at login is a convenience; the G
|
||||
// keys work without it (the daemon is what has to run).
|
||||
if (!gkeymapper::set_autostart_enabled(autostart)) {
|
||||
g_printerr("gkeymapper: autostart entry not %s\n",
|
||||
autostart ? "written" : "removed");
|
||||
}
|
||||
},
|
||||
on_language_selected);
|
||||
}
|
||||
|
||||
static void action_help(GSimpleAction* /*action*/, GVariant* /*parameter*/,
|
||||
gpointer /*userData*/) {
|
||||
gkeymapper::show_help_window(mainWindow != nullptr ? GTK_WINDOW(mainWindow) : nullptr);
|
||||
}
|
||||
|
||||
static void action_options(GSimpleAction* /*action*/, GVariant* /*parameter*/,
|
||||
gpointer /*userData*/) {
|
||||
open_options_dialog();
|
||||
}
|
||||
|
||||
void build_ui(GtkApplication* app) {
|
||||
GtkWidget* window = gtk_application_window_new(app);
|
||||
gtk_window_set_title(GTK_WINDOW(window), _("gKeyMapper – Logitech G910"));
|
||||
@@ -363,10 +594,21 @@ void build_ui(GtkApplication* app) {
|
||||
g_menu_append(menu, _("Export configuration..."), "app.export-config");
|
||||
g_menu_append(menu, _("Import configuration..."), "app.import-config");
|
||||
|
||||
GMenu* optionsSection = g_menu_new();
|
||||
g_menu_append(optionsSection, _("Options"), "app.options");
|
||||
g_menu_append_section(menu, NULL, G_MENU_MODEL(optionsSection));
|
||||
|
||||
GMenu* helpSection = g_menu_new();
|
||||
g_menu_append(helpSection, _("Help"), "app.help");
|
||||
g_menu_append(helpSection, _("About"), "app.about");
|
||||
g_menu_append_section(menu, NULL, G_MENU_MODEL(helpSection));
|
||||
|
||||
// While the window is hidden to the tray the menu is the second way out, so
|
||||
// Quit is there even though closing the window usually does the same.
|
||||
GMenu* quitSection = g_menu_new();
|
||||
g_menu_append(quitSection, _("Quit"), "app.quit");
|
||||
g_menu_append_section(menu, NULL, G_MENU_MODEL(quitSection));
|
||||
|
||||
GtkWidget* menuButton = gtk_menu_button_new();
|
||||
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(menuButton), "open-menu-symbolic");
|
||||
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(menuButton), G_MENU_MODEL(menu));
|
||||
@@ -492,9 +734,11 @@ void build_ui(GtkApplication* app) {
|
||||
gtk_label_set_ellipsize(GTK_LABEL(daemonLabel), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(footer), daemonLabel);
|
||||
|
||||
mainWindow = 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));
|
||||
|
||||
daemonClient.init(on_daemon_status, on_daemon_key);
|
||||
}
|
||||
|
||||
static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
|
||||
@@ -519,11 +763,34 @@ static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
|
||||
g_signal_connect(aboutAction, "activate", G_CALLBACK(action_about), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(aboutAction));
|
||||
|
||||
GSimpleAction* helpAction = g_simple_action_new("help", NULL);
|
||||
g_signal_connect(helpAction, "activate", G_CALLBACK(action_help), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(helpAction));
|
||||
|
||||
GSimpleAction* optionsAction = g_simple_action_new("options", NULL);
|
||||
g_signal_connect(optionsAction, "activate", G_CALLBACK(action_options), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(optionsAction));
|
||||
|
||||
GSimpleAction* quitAction = g_simple_action_new("quit", NULL);
|
||||
g_signal_connect(quitAction, "activate", G_CALLBACK(action_quit), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(quitAction));
|
||||
|
||||
const char* helpAccels[] = {"F1", NULL};
|
||||
gtk_application_set_accels_for_action(app, "app.help", helpAccels);
|
||||
const char* quitAccels[] = {"<Control>q", NULL};
|
||||
gtk_application_set_accels_for_action(app, "app.quit", quitAccels);
|
||||
|
||||
build_ui(app);
|
||||
|
||||
// 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);
|
||||
// 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.
|
||||
g_timeout_add(250, on_minimize_watch, nullptr);
|
||||
daemonClient.init(on_daemon_status, on_daemon_key);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -538,6 +805,14 @@ int main(int argc, char** argv) {
|
||||
bind_textdomain_codeset(PACKAGE, "UTF-8");
|
||||
textdomain(PACKAGE);
|
||||
|
||||
// Window preferences (the tray option, the language) live in their own file
|
||||
// next to the mappings, so a mapping file stays a mapping file.
|
||||
guiSettings = gkeymapper::load_gui_settings();
|
||||
|
||||
// A saved language overrides the session's, and it has to be in place before
|
||||
// the first translated string is built.
|
||||
gkeymapper::apply_gui_language(guiSettings.language);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
namespace gkeymapper {
|
||||
|
||||
bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
|
||||
return save_mappings_file(path, collect_mappings(rows));
|
||||
}
|
||||
|
||||
MappingSet collect_mappings(const std::vector<KeyRow>& rows) {
|
||||
MappingSet set;
|
||||
set.keys.assign(kNumKeys, Mapping{});
|
||||
|
||||
@@ -28,7 +32,7 @@ bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
|
||||
set.keys[row.index] = mapping;
|
||||
}
|
||||
|
||||
return save_mappings_file(path, set);
|
||||
return set;
|
||||
}
|
||||
|
||||
void read_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
|
||||
@@ -38,6 +42,10 @@ void read_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
apply_mappings(set, rows);
|
||||
}
|
||||
|
||||
void apply_mappings(const MappingSet& set, const std::vector<KeyRow>& rows) {
|
||||
for (const KeyRow& row : rows) {
|
||||
if (row.index < 0 || row.index >= static_cast<int>(set.keys.size())) {
|
||||
continue;
|
||||
|
||||
@@ -6,9 +6,17 @@
|
||||
#include <vector>
|
||||
|
||||
#include "keyrow.h"
|
||||
#include "mapping.h"
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// The rows' current state, or a file's content put into the rows. Both are also
|
||||
// what write/read use, so the widget <-> file mapping lives in one place; and a
|
||||
// window that is rebuilt (language switch) carries the rows over this way
|
||||
// without writing a file, so unsaved edits survive.
|
||||
MappingSet collect_mappings(const std::vector<KeyRow>& rows);
|
||||
void apply_mappings(const MappingSet& set, 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);
|
||||
|
||||
|
||||
279
src/optionsdialog.cpp
Normal file
279
src/optionsdialog.cpp
Normal file
@@ -0,0 +1,279 @@
|
||||
// gKeyMapper - the options window.
|
||||
//
|
||||
// Two things in one window: the GUI's own switches (currently the tray
|
||||
// behaviour) and the daemon in detail - the footer says "running", this says
|
||||
// what it runs, which keyboard it found and which file it reads.
|
||||
|
||||
#include "optionsdialog.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
|
||||
// A status line: dim name on the left, value on the right, both readable and
|
||||
// copyable (the config path is worth copying).
|
||||
void add_row(GtkGrid* grid, int row, const char* name, GtkWidget* value) {
|
||||
GtkWidget* nameLabel = gtk_label_new(name);
|
||||
gtk_widget_add_css_class(nameLabel, "dim-label");
|
||||
gtk_label_set_xalign(GTK_LABEL(nameLabel), 0.0f);
|
||||
gtk_grid_attach(grid, nameLabel, 0, row, 1, 1);
|
||||
|
||||
gtk_label_set_xalign(GTK_LABEL(value), 0.0f);
|
||||
gtk_label_set_selectable(GTK_LABEL(value), TRUE);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(value), PANGO_ELLIPSIZE_MIDDLE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(value), 46);
|
||||
gtk_grid_attach(grid, value, 1, row, 1, 1);
|
||||
}
|
||||
|
||||
GtkWidget* value_label() {
|
||||
return gtk_label_new("-");
|
||||
}
|
||||
|
||||
GtkWidget* section_label(const char* text) {
|
||||
GtkWidget* label = gtk_label_new(nullptr);
|
||||
g_autofree gchar* markup = g_strdup_printf("<b>%s</b>", text);
|
||||
gtk_label_set_markup(GTK_LABEL(label), markup);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
return label;
|
||||
}
|
||||
|
||||
// Keeps a value readable when it is longer than the window (a config path).
|
||||
void set_value(GtkWidget* label, const std::string& text) {
|
||||
gtk_label_set_text(GTK_LABEL(label), text.c_str());
|
||||
gtk_widget_set_tooltip_text(label, text.c_str());
|
||||
}
|
||||
|
||||
GtkWidget* hint_label(const char* text) {
|
||||
GtkWidget* label = gtk_label_new(text);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(label), 46);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
return label;
|
||||
}
|
||||
|
||||
// The languages the dropdown offers, in the order of its entries. The first one
|
||||
// is empty: "let the session decide", which is what a fresh install does. The
|
||||
// names are endonyms - they read the same in every language - so they are
|
||||
// deliberately not translated; only the first entry is.
|
||||
const char* const kLanguageCodes[] = {"", "de", "en", "es", "fr"};
|
||||
constexpr guint kNumLanguages = G_N_ELEMENTS(kLanguageCodes);
|
||||
|
||||
} // namespace
|
||||
|
||||
void OptionsDialog::show(GtkWindow* parent, const OptionsState& state, ToggleFn onTray,
|
||||
ToggleFn onAutostart, LanguageFn onLanguage) {
|
||||
onToggled_ = std::move(onTray);
|
||||
onAutostart_ = std::move(onAutostart);
|
||||
onLanguage_ = std::move(onLanguage);
|
||||
trayAvailable_ = state.trayAvailable;
|
||||
minimizeToTray_ = state.settings.minimizeToTray;
|
||||
autostart_ = state.autostart;
|
||||
language_ = state.settings.language;
|
||||
view_ = state.view;
|
||||
|
||||
if (window_ == nullptr) {
|
||||
window_ = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(window_), _("Options"));
|
||||
gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
|
||||
gtk_window_set_icon_name(GTK_WINDOW(window_), "gkeymapper");
|
||||
if (parent != nullptr) {
|
||||
gtk_window_set_transient_for(GTK_WINDOW(window_), parent);
|
||||
}
|
||||
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_top(box, 18);
|
||||
gtk_widget_set_margin_bottom(box, 18);
|
||||
gtk_widget_set_margin_start(box, 18);
|
||||
gtk_widget_set_margin_end(box, 18);
|
||||
gtk_window_set_child(GTK_WINDOW(window_), box);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), section_label(_("Window")));
|
||||
GtkWidget* switchRow = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
||||
GtkWidget* switchLabel = gtk_label_new(_("Minimize to tray"));
|
||||
gtk_widget_set_hexpand(switchLabel, TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(switchLabel), 0.0f);
|
||||
gtk_box_append(GTK_BOX(switchRow), switchLabel);
|
||||
traySwitch_ = gtk_switch_new();
|
||||
gtk_widget_set_valign(traySwitch_, GTK_ALIGN_CENTER);
|
||||
g_signal_connect(traySwitch_, "notify::active", G_CALLBACK(on_tray_switch), this);
|
||||
gtk_box_append(GTK_BOX(switchRow), traySwitch_);
|
||||
gtk_box_append(GTK_BOX(box), switchRow);
|
||||
trayHint_ = hint_label("");
|
||||
gtk_box_append(GTK_BOX(box), trayHint_);
|
||||
|
||||
GtkWidget* autostartRow = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
||||
GtkWidget* autostartLabel = gtk_label_new(_("Start with the session"));
|
||||
gtk_widget_set_hexpand(autostartLabel, TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(autostartLabel), 0.0f);
|
||||
gtk_box_append(GTK_BOX(autostartRow), autostartLabel);
|
||||
autostartSwitch_ = gtk_switch_new();
|
||||
gtk_widget_set_valign(autostartSwitch_, GTK_ALIGN_CENTER);
|
||||
g_signal_connect(autostartSwitch_, "notify::active", G_CALLBACK(on_autostart_switch),
|
||||
this);
|
||||
gtk_box_append(GTK_BOX(autostartRow), autostartSwitch_);
|
||||
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."));
|
||||
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.
|
||||
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);
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), languageDrop_);
|
||||
gtk_box_append(
|
||||
GTK_BOX(box),
|
||||
hint_label(_("The interface switches right away, including the tray menu.")));
|
||||
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_box_append(GTK_BOX(box), section_label(_("Daemon status")));
|
||||
|
||||
GtkWidget* grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 18);
|
||||
gtk_box_append(GTK_BOX(box), grid);
|
||||
|
||||
trayValue_ = value_label();
|
||||
stateValue_ = value_label();
|
||||
versionValue_ = value_label();
|
||||
deviceValue_ = value_label();
|
||||
configValue_ = value_label();
|
||||
lastKeyValue_ = value_label();
|
||||
add_row(GTK_GRID(grid), 0, _("Tray icon"), trayValue_);
|
||||
add_row(GTK_GRID(grid), 1, _("State"), stateValue_);
|
||||
add_row(GTK_GRID(grid), 2, _("Version"), versionValue_);
|
||||
add_row(GTK_GRID(grid), 3, _("Device"), deviceValue_);
|
||||
add_row(GTK_GRID(grid), 4, _("Configuration file"), configValue_);
|
||||
add_row(GTK_GRID(grid), 5, _("Last key"), lastKeyValue_);
|
||||
|
||||
GtkWidget* closeButton = gtk_button_new_with_label(_("Close window"));
|
||||
gtk_widget_set_halign(closeButton, GTK_ALIGN_END);
|
||||
gtk_widget_set_margin_top(closeButton, 6);
|
||||
g_signal_connect_swapped(closeButton, "clicked", G_CALLBACK(gtk_window_destroy), window_);
|
||||
gtk_box_append(GTK_BOX(box), closeButton);
|
||||
|
||||
g_signal_connect(window_, "destroy", G_CALLBACK(on_destroy), this);
|
||||
}
|
||||
|
||||
apply();
|
||||
gtk_window_present(GTK_WINDOW(window_));
|
||||
}
|
||||
|
||||
void OptionsDialog::update(const DaemonView& view) {
|
||||
if (window_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
view_ = view;
|
||||
apply();
|
||||
}
|
||||
|
||||
void OptionsDialog::apply() {
|
||||
filling_ = true;
|
||||
gtk_switch_set_active(GTK_SWITCH(traySwitch_), minimizeToTray_);
|
||||
gtk_widget_set_sensitive(traySwitch_, trayAvailable_);
|
||||
gtk_switch_set_active(GTK_SWITCH(autostartSwitch_), autostart_);
|
||||
guint languageIndex = 0;
|
||||
for (guint index = 0; index < kNumLanguages; ++index) {
|
||||
if (language_ == kLanguageCodes[index]) {
|
||||
languageIndex = index;
|
||||
}
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(languageDrop_), languageIndex);
|
||||
gtk_label_set_text(
|
||||
GTK_LABEL(trayHint_),
|
||||
trayAvailable_
|
||||
? _("Closing or minimizing the window hides it instead of quitting. The tray icon "
|
||||
"shows it again (Show) and quits the program (Close).")
|
||||
: _("No tray is available on this desktop, so the window cannot be hidden."));
|
||||
filling_ = false;
|
||||
|
||||
set_value(trayValue_, trayAvailable_ ? _("active") : _("not available"));
|
||||
set_value(stateValue_, view_.reachable ? _("running") : _("not reachable"));
|
||||
set_value(versionValue_, view_.version.empty() ? "-" : view_.version);
|
||||
set_value(deviceValue_, view_.device.empty() ? _("not found") : view_.device);
|
||||
std::string config = view_.configPath.empty() ? "-" : view_.configPath;
|
||||
if (!view_.configOk) {
|
||||
config += " · ";
|
||||
config += _("Configuration error");
|
||||
}
|
||||
set_value(configValue_, config);
|
||||
set_value(lastKeyValue_, view_.lastKey.empty() ? "-" : view_.lastKey);
|
||||
}
|
||||
|
||||
void OptionsDialog::on_tray_switch(GObject* object, GParamSpec* /*spec*/, gpointer userData) {
|
||||
OptionsDialog* self = static_cast<OptionsDialog*>(userData);
|
||||
if (self->filling_) {
|
||||
return;
|
||||
}
|
||||
// Kept here as well: a daemon update refills the window, and that must not
|
||||
// undo what the user just chose.
|
||||
self->minimizeToTray_ = gtk_switch_get_active(GTK_SWITCH(object)) != FALSE;
|
||||
if (self->onToggled_) {
|
||||
self->onToggled_(self->minimizeToTray_);
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsDialog::on_autostart_switch(GObject* object, GParamSpec* /*spec*/, gpointer userData) {
|
||||
OptionsDialog* self = static_cast<OptionsDialog*>(userData);
|
||||
if (self->filling_) {
|
||||
return;
|
||||
}
|
||||
self->autostart_ = gtk_switch_get_active(GTK_SWITCH(object)) != FALSE;
|
||||
if (self->onAutostart_) {
|
||||
self->onAutostart_(self->autostart_);
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsDialog::on_language_changed(GObject* object, GParamSpec* /*spec*/, gpointer userData) {
|
||||
OptionsDialog* self = static_cast<OptionsDialog*>(userData);
|
||||
if (self->filling_) {
|
||||
return;
|
||||
}
|
||||
const guint index = gtk_drop_down_get_selected(GTK_DROP_DOWN(object));
|
||||
if (index >= kNumLanguages) {
|
||||
return;
|
||||
}
|
||||
self->language_ = kLanguageCodes[index];
|
||||
if (self->onLanguage_) {
|
||||
self->onLanguage_(self->language_);
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsDialog::close() {
|
||||
if (window_ != nullptr) {
|
||||
gtk_window_destroy(GTK_WINDOW(window_));
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsDialog::on_destroy(GtkWidget* /*widget*/, gpointer userData) {
|
||||
OptionsDialog* self = static_cast<OptionsDialog*>(userData);
|
||||
// The widgets go with the window; drop the pointers so show() builds new ones.
|
||||
self->window_ = nullptr;
|
||||
self->traySwitch_ = nullptr;
|
||||
self->trayHint_ = nullptr;
|
||||
self->autostartSwitch_ = nullptr;
|
||||
self->languageDrop_ = nullptr;
|
||||
self->trayValue_ = nullptr;
|
||||
self->stateValue_ = nullptr;
|
||||
self->versionValue_ = nullptr;
|
||||
self->deviceValue_ = nullptr;
|
||||
self->configValue_ = nullptr;
|
||||
self->lastKeyValue_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
83
src/optionsdialog.h
Normal file
83
src/optionsdialog.h
Normal file
@@ -0,0 +1,83 @@
|
||||
// gKeyMapper - the options window.
|
||||
#ifndef GKEYMAPPER_OPTIONSDIALOG_H
|
||||
#define GKEYMAPPER_OPTIONSDIALOG_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include "daemonclient.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// Everything the window shows when it opens. The switches come from different
|
||||
// places - the tray is a bus name, autostart is a file, the rest is the GUI's own
|
||||
// config - so they are collected here instead of being passed one by one.
|
||||
struct OptionsState {
|
||||
bool trayAvailable = false;
|
||||
bool autostart = false;
|
||||
GuiSettings settings;
|
||||
DaemonView view;
|
||||
};
|
||||
|
||||
// The options window: the GUI's own switches plus the full daemon status. It is
|
||||
// deliberately not modal - the footer keeps updating while it is open, and a
|
||||
// switch takes effect immediately.
|
||||
class OptionsDialog {
|
||||
public:
|
||||
using ToggleFn = std::function<void(bool)>;
|
||||
// Called with the chosen language code ("" = follow the session).
|
||||
using LanguageFn = std::function<void(const std::string&)>;
|
||||
|
||||
// Creates the window on first use, afterwards it is re-presented.
|
||||
void show(GtkWindow* parent, const OptionsState& state, ToggleFn onTray,
|
||||
ToggleFn onAutostart, LanguageFn onLanguage);
|
||||
|
||||
// While the window is open: keep the status lines in step with the daemon.
|
||||
void update(const DaemonView& view);
|
||||
|
||||
bool is_open() const { return window_ != nullptr; }
|
||||
|
||||
// Closes it, e.g. before the window is rebuilt for another language.
|
||||
void close();
|
||||
|
||||
private:
|
||||
static void on_tray_switch(GObject* object, GParamSpec* spec, gpointer userData);
|
||||
static void on_autostart_switch(GObject* object, GParamSpec* spec, gpointer userData);
|
||||
static void on_language_changed(GObject* object, GParamSpec* spec, gpointer userData);
|
||||
static void on_destroy(GtkWidget* widget, gpointer userData);
|
||||
|
||||
// Fills the widgets from the state the window was last told about.
|
||||
void apply();
|
||||
|
||||
// Everything the window shows, so a status update does not have to carry the
|
||||
// switches along.
|
||||
bool trayAvailable_ = false;
|
||||
bool minimizeToTray_ = false;
|
||||
bool autostart_ = false;
|
||||
std::string language_;
|
||||
DaemonView view_;
|
||||
|
||||
GtkWidget* window_ = nullptr;
|
||||
GtkWidget* traySwitch_ = nullptr;
|
||||
GtkWidget* trayHint_ = nullptr;
|
||||
GtkWidget* autostartSwitch_ = nullptr;
|
||||
GtkWidget* languageDrop_ = nullptr;
|
||||
GtkWidget* trayValue_ = nullptr;
|
||||
GtkWidget* stateValue_ = nullptr;
|
||||
GtkWidget* versionValue_ = nullptr;
|
||||
GtkWidget* deviceValue_ = nullptr;
|
||||
GtkWidget* configValue_ = nullptr;
|
||||
GtkWidget* lastKeyValue_ = nullptr;
|
||||
// Set while the window fills its widgets, so programmatic changes do not look
|
||||
// like the user flipping a switch.
|
||||
bool filling_ = false;
|
||||
ToggleFn onToggled_;
|
||||
ToggleFn onAutostart_;
|
||||
LanguageFn onLanguage_;
|
||||
};
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_OPTIONSDIALOG_H
|
||||
95
src/settings.cpp
Normal file
95
src/settings.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
// gKeyMapper - GUI options that are not part of the mapping format.
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
#include <clocale>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
// Same shape as the mapping file next to it: a group and one key per option.
|
||||
const char* kGroup = "gui";
|
||||
const char* kMinimizeToTray = "minimize-to-tray";
|
||||
const char* kLanguage = "language";
|
||||
|
||||
// What the session had in LANGUAGE before this program touched it, so choosing
|
||||
// "follow the session" can put it back exactly.
|
||||
std::string sessionLanguage;
|
||||
bool sessionLanguageTaken = false;
|
||||
} // namespace
|
||||
|
||||
std::string default_gui_config_path() {
|
||||
const char* home = std::getenv("HOME");
|
||||
return std::string(home ? home : "") + "/.config/gKeyMapper/gui.conf";
|
||||
}
|
||||
|
||||
GuiSettings load_gui_settings() {
|
||||
GuiSettings settings;
|
||||
GKeyFile* file = g_key_file_new();
|
||||
GError* error = nullptr;
|
||||
// A missing file is the normal first start, not an error.
|
||||
if (g_key_file_load_from_file(file, default_gui_config_path().c_str(), G_KEY_FILE_NONE,
|
||||
&error)) {
|
||||
GError* itemError = nullptr;
|
||||
const gboolean value = g_key_file_get_boolean(file, kGroup, kMinimizeToTray, &itemError);
|
||||
if (itemError == nullptr) {
|
||||
settings.minimizeToTray = value != FALSE;
|
||||
}
|
||||
g_clear_error(&itemError);
|
||||
|
||||
g_autofree gchar* language = g_key_file_get_string(file, kGroup, kLanguage, nullptr);
|
||||
if (language != nullptr) {
|
||||
settings.language = language;
|
||||
}
|
||||
}
|
||||
g_clear_error(&error);
|
||||
g_key_file_unref(file);
|
||||
return settings;
|
||||
}
|
||||
|
||||
void save_gui_settings(const GuiSettings& settings) {
|
||||
const std::string path = default_gui_config_path();
|
||||
g_autofree gchar* directory = g_path_get_dirname(path.c_str());
|
||||
g_mkdir_with_parents(directory, 0700);
|
||||
|
||||
GKeyFile* file = g_key_file_new();
|
||||
g_key_file_set_boolean(file, kGroup, kMinimizeToTray, settings.minimizeToTray);
|
||||
g_key_file_set_string(file, kGroup, kLanguage, settings.language.c_str());
|
||||
GError* error = nullptr;
|
||||
if (!g_key_file_save_to_file(file, path.c_str(), &error)) {
|
||||
g_printerr("gkeymapper: could not write %s: %s\n", path.c_str(),
|
||||
error != nullptr ? error->message : "unknown error");
|
||||
g_clear_error(&error);
|
||||
}
|
||||
g_key_file_unref(file);
|
||||
}
|
||||
|
||||
void apply_gui_language(const std::string& language) {
|
||||
if (!sessionLanguageTaken) {
|
||||
const char* current = g_getenv("LANGUAGE");
|
||||
sessionLanguage = current != nullptr ? current : "";
|
||||
sessionLanguageTaken = true;
|
||||
}
|
||||
|
||||
// gettext checks LANGUAGE before the locale, so it is the one knob to turn;
|
||||
// the shipped catalogs are small and the identity catalog for English means
|
||||
// an explicit "en" works on a German desktop too.
|
||||
if (language.empty()) {
|
||||
if (sessionLanguage.empty()) {
|
||||
g_unsetenv("LANGUAGE");
|
||||
} else {
|
||||
g_setenv("LANGUAGE", sessionLanguage.c_str(), TRUE);
|
||||
}
|
||||
} else {
|
||||
g_setenv("LANGUAGE", language.c_str(), TRUE);
|
||||
}
|
||||
|
||||
// setlocale re-reads the environment and bumps glibc's catalog counter, so
|
||||
// the next translated string comes out of the new catalog.
|
||||
setlocale(LC_ALL, "");
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
33
src/settings.h
Normal file
33
src/settings.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// gKeyMapper - GUI options that are not part of the mapping format.
|
||||
#ifndef GKEYMAPPER_SETTINGS_H
|
||||
#define GKEYMAPPER_SETTINGS_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// What the GUI remembers between runs. It lives in `gui.conf` next to
|
||||
// `mappings.conf`, so a mapping file can be exported, imported or copied around
|
||||
// without dragging a window preference with it.
|
||||
struct GuiSettings {
|
||||
// Closing or minimizing the window hides it to the tray instead of quitting.
|
||||
bool minimizeToTray = false;
|
||||
// Language of the interface, as a two-letter code from the shipped catalogs
|
||||
// ("de", "en", "es", "fr"). Empty means "follow the session" - the usual
|
||||
// case, where LANG/LC_MESSAGES decide.
|
||||
std::string language;
|
||||
};
|
||||
|
||||
std::string default_gui_config_path();
|
||||
GuiSettings load_gui_settings();
|
||||
void save_gui_settings(const GuiSettings& settings);
|
||||
|
||||
// Hands the language to gettext and re-reads the locale. Must run before the
|
||||
// first translated string is fetched; an empty `language` restores what the
|
||||
// session had. Already built widgets keep their texts, so the caller rebuilds
|
||||
// them (see the language switch in main.cpp).
|
||||
void apply_gui_language(const std::string& language);
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_SETTINGS_H
|
||||
635
src/trayicon.cpp
Normal file
635
src/trayicon.cpp
Normal file
@@ -0,0 +1,635 @@
|
||||
// gKeyMapper - the tray icon, spoken as a StatusNotifierItem.
|
||||
//
|
||||
// Two interfaces are exported: the item itself (what the panel shows and where a
|
||||
// click lands) and its menu (com.canonical.dbusmenu, the shape every tray menu on
|
||||
// Linux has). Nothing here knows about GTK - it is plain GIO, so the icon works
|
||||
// the same on any desktop that hosts the protocol.
|
||||
|
||||
#include "trayicon.h"
|
||||
|
||||
#include <unistd.h> // getpid
|
||||
|
||||
#include <glib/gi18n.h>
|
||||
#include <glib.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kItemPath = "/StatusNotifierItem";
|
||||
const char* kMenuPath = "/MenuBar";
|
||||
// The two interfaces, spelled out: they are what the panel talks to.
|
||||
const char* kItemInterface = "org.kde.StatusNotifierItem";
|
||||
const char* kMenuInterface = "com.canonical.dbusmenu";
|
||||
const char* kWatcherName = "org.kde.StatusNotifierWatcher";
|
||||
// Some desktops publish the watcher only under this older alias (libxapp owns
|
||||
// both names; the interface is the standard one either way).
|
||||
const char* kWatcherNameX = "org.x.StatusNotifierWatcher";
|
||||
const char* kWatcherPath = "/StatusNotifierWatcher";
|
||||
const char* kWatcherInterface = "org.kde.StatusNotifierWatcher";
|
||||
|
||||
// Menu ids. 0 is the root the panel asks for; it never is an entry itself.
|
||||
constexpr int kRootId = 0;
|
||||
constexpr int kShowId = 1;
|
||||
constexpr int kCloseId = 2;
|
||||
|
||||
// Item: what the icon looks like and where the menu is. The properties the panel
|
||||
// asks for are the ones answered in on_item_get_property().
|
||||
const char* kItemXml =
|
||||
"<node>"
|
||||
" <interface name='org.kde.StatusNotifierItem'>"
|
||||
" <property name='Category' type='s' access='read'/>"
|
||||
" <property name='Id' type='s' access='read'/>"
|
||||
" <property name='Title' type='s' access='read'/>"
|
||||
" <property name='Status' type='s' access='read'/>"
|
||||
" <property name='IconName' type='s' access='read'/>"
|
||||
" <property name='IconPixmap' type='a(iiay)' access='read'/>"
|
||||
" <property name='OverlayIconName' type='s' access='read'/>"
|
||||
" <property name='AttentionIconName' type='s' access='read'/>"
|
||||
" <property name='ToolTip' type='(sa(iiay)ss)' access='read'/>"
|
||||
" <property name='ItemIsMenu' type='b' access='read'/>"
|
||||
" <property name='Menu' type='o' access='read'/>"
|
||||
" <method name='Activate'>"
|
||||
" <arg name='x' type='i' direction='in'/>"
|
||||
" <arg name='y' type='i' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='SecondaryActivate'>"
|
||||
" <arg name='x' type='i' direction='in'/>"
|
||||
" <arg name='y' type='i' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='ContextMenu'>"
|
||||
" <arg name='x' type='i' direction='in'/>"
|
||||
" <arg name='y' type='i' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='Scroll'>"
|
||||
" <arg name='delta' type='i' direction='in'/>"
|
||||
" <arg name='orientation' type='s' direction='in'/>"
|
||||
" </method>"
|
||||
" <signal name='NewIcon'/>"
|
||||
" <signal name='NewStatus'>"
|
||||
" <arg name='status' type='s'/>"
|
||||
" </signal>"
|
||||
" <signal name='NewToolTip'/>"
|
||||
" </interface>"
|
||||
"</node>";
|
||||
|
||||
// Menu: only what a host actually calls. The two entries are added in
|
||||
// layout(); nothing about them changes unless the language does, and for that
|
||||
// refresh_labels() bumps the revision and says so.
|
||||
const char* kMenuXml =
|
||||
"<node>"
|
||||
" <interface name='com.canonical.dbusmenu'>"
|
||||
" <method name='GetLayout'>"
|
||||
" <arg name='parentId' type='i' direction='in'/>"
|
||||
" <arg name='recursionDepth' type='i' direction='in'/>"
|
||||
" <arg name='propertyNames' type='as' direction='in'/>"
|
||||
" <arg name='revision' type='u' direction='out'/>"
|
||||
" <arg name='layout' type='(ia{sv}av)' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='GetGroupProperties'>"
|
||||
" <arg name='ids' type='ai' direction='in'/>"
|
||||
" <arg name='propertyNames' type='as' direction='in'/>"
|
||||
" <arg name='properties' type='a(ia{sv})' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='GetProperty'>"
|
||||
" <arg name='id' type='i' direction='in'/>"
|
||||
" <arg name='name' type='s' direction='in'/>"
|
||||
" <arg name='value' type='v' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='Event'>"
|
||||
" <arg name='id' type='i' direction='in'/>"
|
||||
" <arg name='eventId' type='s' direction='in'/>"
|
||||
" <arg name='data' type='v' direction='in'/>"
|
||||
" <arg name='timestamp' type='u' direction='in'/>"
|
||||
" </method>"
|
||||
" <method name='EventGroup'>"
|
||||
" <arg name='events' type='a(isvu)' direction='in'/>"
|
||||
" <arg name='idErrors' type='ai' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='AboutToShow'>"
|
||||
" <arg name='id' type='i' direction='in'/>"
|
||||
" <arg name='needUpdate' type='b' direction='out'/>"
|
||||
" </method>"
|
||||
" <method name='AboutToShowGroup'>"
|
||||
" <arg name='ids' type='ai' direction='in'/>"
|
||||
" <arg name='updatesNeeded' type='ai' direction='out'/>"
|
||||
" <arg name='idErrors' type='ai' direction='out'/>"
|
||||
" </method>"
|
||||
" <signal name='ItemsPropertiesUpdated'>"
|
||||
" <arg name='updatedProps' type='a(ia{sv})'/>"
|
||||
" <arg name='removedProps' type='a(ias)'/>"
|
||||
" </signal>"
|
||||
" <signal name='LayoutUpdated'>"
|
||||
" <arg name='revision' type='u'/>"
|
||||
" <arg name='parent' type='i'/>"
|
||||
" </signal>"
|
||||
" <property name='Version' type='u' access='read'/>"
|
||||
" <property name='TextDirection' type='s' access='read'/>"
|
||||
" <property name='Status' type='s' access='read'/>"
|
||||
" <property name='IconThemePath' type='as' access='read'/>"
|
||||
" </interface>"
|
||||
"</node>";
|
||||
|
||||
// What a menu entry says. The panel draws them, so they are translated here.
|
||||
const char* entry_label(int id) {
|
||||
if (id == kShowId) {
|
||||
return _("Show");
|
||||
}
|
||||
if (id == kCloseId) {
|
||||
return _("Close");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
gboolean run_later(gpointer data) {
|
||||
auto* action = static_cast<std::function<void()>*>(data);
|
||||
(*action)();
|
||||
delete action;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// Answers a D-Bus method in this loop turn and runs the handler afterwards: a
|
||||
// quit must not close the connection before the reply is on the wire.
|
||||
void call_later(TrayIcon::Callback callback) {
|
||||
if (callback) {
|
||||
g_idle_add(run_later, new TrayIcon::Callback(std::move(callback)));
|
||||
}
|
||||
}
|
||||
|
||||
void reply_empty(GDBusMethodInvocation* invocation) {
|
||||
g_dbus_method_invocation_return_value(invocation, nullptr);
|
||||
}
|
||||
|
||||
GVariant* empty_string_array() {
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
|
||||
return g_variant_builder_end(&builder);
|
||||
}
|
||||
|
||||
GVariant* empty_int_array() {
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("ai"));
|
||||
return g_variant_builder_end(&builder);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TrayIcon::~TrayIcon() {
|
||||
if (watcherId_ != 0) {
|
||||
g_bus_unwatch_name(watcherId_);
|
||||
}
|
||||
if (watcherIdX_ != 0) {
|
||||
g_bus_unwatch_name(watcherIdX_);
|
||||
}
|
||||
if (connection_ != nullptr) {
|
||||
if (itemRegistration_ != 0) {
|
||||
g_dbus_connection_unregister_object(connection_, itemRegistration_);
|
||||
}
|
||||
if (menuRegistration_ != 0) {
|
||||
g_dbus_connection_unregister_object(connection_, menuRegistration_);
|
||||
}
|
||||
}
|
||||
if (ownerId_ != 0) {
|
||||
g_bus_unown_name(ownerId_);
|
||||
}
|
||||
}
|
||||
|
||||
void TrayIcon::start(std::vector<unsigned char> icon, int width, int height, Callback onShow,
|
||||
Callback onQuit) {
|
||||
icon_ = std::move(icon);
|
||||
iconWidth_ = width;
|
||||
iconHeight_ = height;
|
||||
onShow_ = std::move(onShow);
|
||||
onQuit_ = std::move(onQuit);
|
||||
|
||||
// The name is what the panel reports to its user, so it says what the icon
|
||||
// is; the pid keeps two sessions apart.
|
||||
g_autofree gchar* name = g_strdup_printf("org.kde.StatusNotifierItem-%d-1", getpid());
|
||||
serviceName_ = name;
|
||||
ownerId_ = g_bus_own_name(G_BUS_TYPE_SESSION, serviceName_.c_str(),
|
||||
G_BUS_NAME_OWNER_FLAGS_NONE, on_bus_acquired, nullptr, on_name_lost,
|
||||
this, nullptr);
|
||||
// Watch both watcher names: the panel's own one and the older alias. Each
|
||||
// callback fires right away when the name is already owned.
|
||||
watcherId_ = g_bus_watch_name(G_BUS_TYPE_SESSION, kWatcherName,
|
||||
G_BUS_NAME_WATCHER_FLAGS_NONE, on_watcher_appeared,
|
||||
on_watcher_vanished, this, nullptr);
|
||||
watcherIdX_ = g_bus_watch_name(G_BUS_TYPE_SESSION, kWatcherNameX,
|
||||
G_BUS_NAME_WATCHER_FLAGS_NONE, on_watcher_appeared,
|
||||
on_watcher_vanished, this, nullptr);
|
||||
}
|
||||
|
||||
void TrayIcon::on_bus_acquired(GDBusConnection* connection, const gchar* /*name*/,
|
||||
gpointer userData) {
|
||||
static_cast<TrayIcon*>(userData)->export_objects(connection);
|
||||
}
|
||||
|
||||
void TrayIcon::on_name_lost(GDBusConnection* /*connection*/, const gchar* name,
|
||||
gpointer /*userData*/) {
|
||||
// Being unique (see singleinstance) this should not happen; if it does, the
|
||||
// panel cannot reach us and the icon is simply absent.
|
||||
g_printerr("gkeymapper: tray icon could not take %s\n", name);
|
||||
}
|
||||
|
||||
void TrayIcon::on_watcher_appeared(GDBusConnection* /*connection*/, const gchar* name,
|
||||
const gchar* /*nameOwner*/, gpointer userData) {
|
||||
static_cast<TrayIcon*>(userData)->set_watcher_available(name, true);
|
||||
}
|
||||
|
||||
void TrayIcon::on_watcher_vanished(GDBusConnection* /*connection*/, const gchar* name,
|
||||
gpointer userData) {
|
||||
static_cast<TrayIcon*>(userData)->set_watcher_available(name, false);
|
||||
}
|
||||
|
||||
void TrayIcon::set_watcher_available(const char* name, bool available) {
|
||||
if (g_strcmp0(name, kWatcherNameX) == 0) {
|
||||
watcherAlias_ = available;
|
||||
} else {
|
||||
watcher_ = available;
|
||||
}
|
||||
|
||||
if (!available) {
|
||||
// The panel (or its watcher) went away: the icon is not on screen any
|
||||
// more, and hiding the window would strand the user.
|
||||
if (registeredWith_ == name) {
|
||||
registered_ = false;
|
||||
registeredWith_.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
register_with_watcher();
|
||||
}
|
||||
|
||||
void TrayIcon::export_objects(GDBusConnection* connection) {
|
||||
connection_ = connection;
|
||||
|
||||
GError* error = nullptr;
|
||||
g_autoptr(GDBusNodeInfo) itemNode = g_dbus_node_info_new_for_xml(kItemXml, &error);
|
||||
if (itemNode == nullptr) {
|
||||
g_printerr("gkeymapper: tray item: %s\n", error != nullptr ? error->message : "?");
|
||||
g_clear_error(&error);
|
||||
return;
|
||||
}
|
||||
static const GDBusInterfaceVTable itemVtable = {
|
||||
on_item_method_call, on_item_get_property, nullptr,
|
||||
{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
itemRegistration_ = g_dbus_connection_register_object(
|
||||
connection, kItemPath, itemNode->interfaces[0], &itemVtable, this, nullptr, &error);
|
||||
if (itemRegistration_ == 0) {
|
||||
g_printerr("gkeymapper: tray item: %s\n", error != nullptr ? error->message : "?");
|
||||
g_clear_error(&error);
|
||||
return;
|
||||
}
|
||||
|
||||
g_autoptr(GDBusNodeInfo) menuNode = g_dbus_node_info_new_for_xml(kMenuXml, &error);
|
||||
if (menuNode == nullptr) {
|
||||
g_printerr("gkeymapper: tray menu: %s\n", error != nullptr ? error->message : "?");
|
||||
g_clear_error(&error);
|
||||
return;
|
||||
}
|
||||
static const GDBusInterfaceVTable menuVtable = {
|
||||
on_menu_method_call, on_menu_get_property, nullptr,
|
||||
{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
menuRegistration_ = g_dbus_connection_register_object(
|
||||
connection, kMenuPath, menuNode->interfaces[0], &menuVtable, this, nullptr, &error);
|
||||
if (menuRegistration_ == 0) {
|
||||
g_printerr("gkeymapper: tray menu: %s\n", error != nullptr ? error->message : "?");
|
||||
g_clear_error(&error);
|
||||
}
|
||||
}
|
||||
|
||||
void TrayIcon::register_with_watcher() {
|
||||
if (connection_ == nullptr || serviceName_.empty() || registering_) {
|
||||
return;
|
||||
}
|
||||
// Both names are normally owned by the same process, so preferring the
|
||||
// standard one keeps it at exactly one registration (and one icon).
|
||||
// Already registered there, and the name we used is still on the bus: leave it
|
||||
// at that, so a second watcher name does not add the item twice.
|
||||
if (registered_ && ((registeredWith_ == kWatcherName && watcher_) ||
|
||||
(registeredWith_ == kWatcherNameX && watcherAlias_))) {
|
||||
return;
|
||||
}
|
||||
const char* target = nullptr;
|
||||
if (watcher_) {
|
||||
target = kWatcherName;
|
||||
} else if (watcherAlias_) {
|
||||
target = kWatcherNameX;
|
||||
}
|
||||
if (target == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The watcher is told our bus name and reads the properties from there. The
|
||||
// item path is the fixed one, so the name is enough.
|
||||
registering_ = true;
|
||||
registeringWith_ = target;
|
||||
g_dbus_connection_call(connection_, target, kWatcherPath, kWatcherInterface,
|
||||
"RegisterStatusNotifierItem", g_variant_new("(s)", serviceName_.c_str()),
|
||||
nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, on_register_reply, this);
|
||||
}
|
||||
|
||||
void TrayIcon::on_register_reply(GObject* source, GAsyncResult* result, gpointer userData) {
|
||||
TrayIcon* self = static_cast<TrayIcon*>(userData);
|
||||
self->registering_ = false;
|
||||
|
||||
GError* error = nullptr;
|
||||
g_autoptr(GVariant) reply =
|
||||
g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
|
||||
if (reply == nullptr) {
|
||||
// No panel, or it went away while we asked: the icon is simply absent.
|
||||
g_printerr("gkeymapper: no tray icon: %s\n", error != nullptr ? error->message : "?");
|
||||
g_clear_error(&error);
|
||||
self->registered_ = false;
|
||||
self->registeredWith_.clear();
|
||||
return;
|
||||
}
|
||||
self->registered_ = true;
|
||||
self->registeredWith_ = self->registeringWith_;
|
||||
|
||||
// A watcher may have appeared while the call was in flight (the panel
|
||||
// restarting, for instance); take the other name into account once more.
|
||||
self->register_with_watcher();
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::icon_pixmaps() const {
|
||||
// a(iiay): the protocol allows several sizes, one is enough here. The bytes
|
||||
// are ARGB32 in network byte order, i.e. A first.
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("a(iiay)"));
|
||||
if (!icon_.empty() && iconWidth_ > 0 && iconHeight_ > 0) {
|
||||
GVariant* pixels =
|
||||
g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, icon_.data(), icon_.size(), 1);
|
||||
g_variant_builder_add(&builder, "(ii@ay)", iconWidth_, iconHeight_, pixels);
|
||||
}
|
||||
return g_variant_builder_end(&builder);
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::tooltip() const {
|
||||
// (icon name, pixmaps, title, text): the panel shows title and text.
|
||||
return g_variant_new("(s@a(iiay)ss)", "", icon_pixmaps(), _("gKeyMapper"),
|
||||
_("G-Key mapper for the Logitech G910 (GTK4)."));
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::layout(int parentId, int depth) const {
|
||||
return g_variant_new("(u@(ia{sv}av))", menuRevision_, node(parentId, depth));
|
||||
}
|
||||
|
||||
void TrayIcon::refresh_labels() {
|
||||
if (connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
++menuRevision_;
|
||||
|
||||
// The watcher mirrors the layout when the item registers and asks again when
|
||||
// it hears LayoutUpdated; the tooltip is plain text as well.
|
||||
GError* error = nullptr;
|
||||
g_dbus_connection_emit_signal(connection_, nullptr, kMenuPath, kMenuInterface, "LayoutUpdated",
|
||||
g_variant_new("(ui)", menuRevision_, kRootId), &error);
|
||||
if (error != nullptr) {
|
||||
g_printerr("gkeymapper: could not announce the new menu labels: %s\n", error->message);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
g_dbus_connection_emit_signal(connection_, nullptr, kItemPath, kItemInterface, "NewToolTip",
|
||||
nullptr, &error);
|
||||
if (error != nullptr) {
|
||||
g_printerr("gkeymapper: could not announce the new tooltip: %s\n", error->message);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
}
|
||||
|
||||
// One node of the menu tree: (id, properties, children). The children are again
|
||||
// full nodes - the panel walks them with libdbusmenu, and a bare id in there makes
|
||||
// that parser abort. `depth` 0 means "no children below this one".
|
||||
GVariant* TrayIcon::node(int id, int depth) const {
|
||||
GVariantBuilder properties;
|
||||
g_variant_builder_init(&properties, G_VARIANT_TYPE("a{sv}"));
|
||||
GVariantBuilder children;
|
||||
g_variant_builder_init(&children, G_VARIANT_TYPE("av"));
|
||||
|
||||
if (id == kRootId) {
|
||||
// The root is not shown itself; "children-display" is what tells the panel
|
||||
// to pop the entries up as a submenu.
|
||||
g_variant_builder_add(&properties, "{sv}", "children-display",
|
||||
g_variant_new_string("submenu"));
|
||||
if (depth != 0) {
|
||||
g_variant_builder_add(&children, "v", node(kShowId, depth - 1));
|
||||
g_variant_builder_add(&children, "v", node(kCloseId, depth - 1));
|
||||
}
|
||||
} else {
|
||||
g_variant_builder_add(&properties, "{sv}", "label",
|
||||
g_variant_new_string(entry_label(id)));
|
||||
g_variant_builder_add(&properties, "{sv}", "enabled", g_variant_new_boolean(TRUE));
|
||||
g_variant_builder_add(&properties, "{sv}", "visible", g_variant_new_boolean(TRUE));
|
||||
}
|
||||
|
||||
return g_variant_new("(i@a{sv}@av)", id, g_variant_builder_end(&properties),
|
||||
g_variant_builder_end(&children));
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::group_properties(GVariant* ids) const {
|
||||
GVariantBuilder result;
|
||||
g_variant_builder_init(&result, G_VARIANT_TYPE("a(ia{sv})"));
|
||||
|
||||
const gsize count = g_variant_n_children(ids);
|
||||
const int first = count == 0 ? kRootId : 0;
|
||||
const int last = count == 0 ? kCloseId : static_cast<int>(count) - 1;
|
||||
for (int index = first; index <= last; ++index) {
|
||||
int id = index;
|
||||
if (count != 0) {
|
||||
g_variant_get_child(ids, index, "i", &id);
|
||||
}
|
||||
GVariantBuilder properties;
|
||||
g_variant_builder_init(&properties, G_VARIANT_TYPE("a{sv}"));
|
||||
if (id == kRootId) {
|
||||
g_variant_builder_add(&properties, "{sv}", "children-display",
|
||||
g_variant_new_string("submenu"));
|
||||
} else {
|
||||
g_variant_builder_add(&properties, "{sv}", "label",
|
||||
g_variant_new_string(entry_label(id)));
|
||||
g_variant_builder_add(&properties, "{sv}", "enabled", g_variant_new_boolean(TRUE));
|
||||
g_variant_builder_add(&properties, "{sv}", "visible", g_variant_new_boolean(TRUE));
|
||||
}
|
||||
g_variant_builder_add(&result, "(i@a{sv})", id, g_variant_builder_end(&properties));
|
||||
}
|
||||
return g_variant_builder_end(&result);
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::menu_property(int id, const char* name) const {
|
||||
const std::string property = name != nullptr ? name : "";
|
||||
if (property == "label") {
|
||||
return g_variant_new_string(entry_label(id));
|
||||
}
|
||||
if (property == "enabled" || property == "visible") {
|
||||
return g_variant_new_boolean(TRUE);
|
||||
}
|
||||
if (property == "children-display") {
|
||||
return g_variant_new_string(id == kRootId ? "submenu" : "");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TrayIcon::on_item_method_call(GDBusConnection* /*connection*/, const gchar* /*sender*/,
|
||||
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
|
||||
const gchar* methodName, GVariant* /*parameters*/,
|
||||
GDBusMethodInvocation* invocation, gpointer userData) {
|
||||
TrayIcon* self = static_cast<TrayIcon*>(userData);
|
||||
const std::string method = methodName;
|
||||
if (method == "Activate") {
|
||||
// A click on the icon: bring the window back.
|
||||
call_later(self->onShow_);
|
||||
}
|
||||
// SecondaryActivate, ContextMenu and Scroll need no answer of their own: the
|
||||
// panel pops the menu itself.
|
||||
reply_empty(invocation);
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::on_item_get_property(GDBusConnection* /*connection*/, const gchar* /*sender*/,
|
||||
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
|
||||
const gchar* propertyName, GError** error,
|
||||
gpointer userData) {
|
||||
TrayIcon* self = static_cast<TrayIcon*>(userData);
|
||||
const std::string property = propertyName;
|
||||
if (property == "Category") {
|
||||
return g_variant_new_string("ApplicationStatus");
|
||||
}
|
||||
if (property == "Id") {
|
||||
return g_variant_new_string("gkeymapper");
|
||||
}
|
||||
if (property == "Title") {
|
||||
return g_variant_new_string(_("gKeyMapper"));
|
||||
}
|
||||
if (property == "Status") {
|
||||
// Always "Active": the icon must not disappear when the window is hidden.
|
||||
return g_variant_new_string("Active");
|
||||
}
|
||||
if (property == "IconName" || property == "OverlayIconName" ||
|
||||
property == "AttentionIconName") {
|
||||
return g_variant_new_string("");
|
||||
}
|
||||
if (property == "IconPixmap") {
|
||||
return self->icon_pixmaps();
|
||||
}
|
||||
if (property == "ToolTip") {
|
||||
return self->tooltip();
|
||||
}
|
||||
if (property == "ItemIsMenu") {
|
||||
return g_variant_new_boolean(FALSE);
|
||||
}
|
||||
if (property == "Menu") {
|
||||
return g_variant_new_object_path(kMenuPath);
|
||||
}
|
||||
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "unknown property %s",
|
||||
propertyName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TrayIcon::handle_event(int id, const char* eventId) {
|
||||
if (g_strcmp0(eventId, "clicked") != 0) {
|
||||
return;
|
||||
}
|
||||
if (id == kShowId) {
|
||||
call_later(onShow_);
|
||||
} else if (id == kCloseId) {
|
||||
call_later(onQuit_);
|
||||
}
|
||||
}
|
||||
|
||||
void TrayIcon::on_menu_method_call(GDBusConnection* /*connection*/, const gchar* /*sender*/,
|
||||
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
|
||||
const gchar* methodName, GVariant* parameters,
|
||||
GDBusMethodInvocation* invocation, gpointer userData) {
|
||||
TrayIcon* self = static_cast<TrayIcon*>(userData);
|
||||
const std::string method = methodName;
|
||||
|
||||
if (method == "GetLayout") {
|
||||
gint32 parentId = kRootId;
|
||||
gint32 depth = -1; // -1: as deep as it goes
|
||||
g_autoptr(GVariant) propertyNames = nullptr;
|
||||
g_variant_get(parameters, "(ii@as)", &parentId, &depth, &propertyNames);
|
||||
g_dbus_method_invocation_return_value(invocation, self->layout(parentId, depth));
|
||||
return;
|
||||
}
|
||||
if (method == "GetGroupProperties") {
|
||||
g_autoptr(GVariant) ids = nullptr;
|
||||
g_autoptr(GVariant) propertyNames = nullptr;
|
||||
g_variant_get(parameters, "(@ai@as)", &ids, &propertyNames);
|
||||
GVariant* properties = self->group_properties(ids);
|
||||
g_dbus_method_invocation_return_value(invocation, g_variant_new("(@a(ia{sv}))", properties));
|
||||
return;
|
||||
}
|
||||
if (method == "GetProperty") {
|
||||
gint32 id = kRootId;
|
||||
const gchar* name = nullptr;
|
||||
g_variant_get(parameters, "(i&s)", &id, &name);
|
||||
GVariant* value = self->menu_property(id, name);
|
||||
if (value == nullptr) {
|
||||
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
|
||||
G_DBUS_ERROR_UNKNOWN_PROPERTY,
|
||||
"unknown property %s", name != nullptr ? name : "");
|
||||
return;
|
||||
}
|
||||
g_dbus_method_invocation_return_value(invocation, g_variant_new("(v)", value));
|
||||
return;
|
||||
}
|
||||
if (method == "Event") {
|
||||
gint32 id = kRootId;
|
||||
const gchar* eventId = nullptr;
|
||||
g_variant_get(parameters, "(i&svu)", &id, &eventId, nullptr, nullptr);
|
||||
self->handle_event(id, eventId);
|
||||
reply_empty(invocation);
|
||||
return;
|
||||
}
|
||||
if (method == "EventGroup") {
|
||||
// A batch of clicks in one call. The panel's watcher (libdbusmenu) sends
|
||||
// its events this way, so this is the path a real click takes.
|
||||
g_autoptr(GVariant) events = nullptr;
|
||||
g_variant_get(parameters, "(@a(isvu))", &events);
|
||||
const gsize count = g_variant_n_children(events);
|
||||
for (gsize index = 0; index < count; ++index) {
|
||||
gint32 id = kRootId;
|
||||
const gchar* eventId = nullptr;
|
||||
g_autoptr(GVariant) data = nullptr;
|
||||
g_variant_get_child(events, index, "(i&s@vu)", &id, &eventId, &data, nullptr);
|
||||
self->handle_event(id, eventId);
|
||||
}
|
||||
g_dbus_method_invocation_return_value(invocation,
|
||||
g_variant_new("(@ai)", empty_int_array()));
|
||||
return;
|
||||
}
|
||||
if (method == "AboutToShow") {
|
||||
// Nothing changes between two openings, so nothing needs an update.
|
||||
g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", FALSE));
|
||||
return;
|
||||
}
|
||||
if (method == "AboutToShowGroup") {
|
||||
g_dbus_method_invocation_return_value(
|
||||
invocation, g_variant_new("(@ai@ai)", empty_int_array(), empty_int_array()));
|
||||
return;
|
||||
}
|
||||
reply_empty(invocation);
|
||||
}
|
||||
|
||||
GVariant* TrayIcon::on_menu_get_property(GDBusConnection* /*connection*/, const gchar* /*sender*/,
|
||||
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
|
||||
const gchar* propertyName, GError** error,
|
||||
gpointer /*userData*/) {
|
||||
const std::string property = propertyName;
|
||||
if (property == "Version") {
|
||||
return g_variant_new_uint32(3);
|
||||
}
|
||||
if (property == "TextDirection") {
|
||||
return g_variant_new_string("ltr");
|
||||
}
|
||||
if (property == "Status") {
|
||||
return g_variant_new_string("normal");
|
||||
}
|
||||
if (property == "IconThemePath") {
|
||||
return empty_string_array();
|
||||
}
|
||||
g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "unknown property %s",
|
||||
propertyName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
109
src/trayicon.h
Normal file
109
src/trayicon.h
Normal file
@@ -0,0 +1,109 @@
|
||||
// gKeyMapper - the tray icon, spoken as a StatusNotifierItem.
|
||||
#ifndef GKEYMAPPER_TRAYICON_H
|
||||
#define GKEYMAPPER_TRAYICON_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// The tray icon. GTK4 has no tray widget (GtkStatusIcon was GTK3 and is gone), so
|
||||
// the icon is exported on the session bus as a StatusNotifierItem plus a
|
||||
// com.canonical.dbusmenu menu - exactly the protocol libxapp and libappindicator
|
||||
// wrap, only without their GTK3 dependency (a GTK4 process must not load GTK3).
|
||||
// The panel hosts it: Cinnamon's xapp-status applet, KDE, Unity, or the GNOME
|
||||
// AppIndicator extension.
|
||||
//
|
||||
// The panel is a different process, so the icon travels as a pixmap: it cannot
|
||||
// read the icons compiled into this binary.
|
||||
class TrayIcon {
|
||||
public:
|
||||
using Callback = std::function<void()>;
|
||||
|
||||
TrayIcon() = default;
|
||||
~TrayIcon();
|
||||
TrayIcon(const TrayIcon&) = delete;
|
||||
TrayIcon& operator=(const TrayIcon&) = delete;
|
||||
|
||||
// Owns the item's bus name, exports the item and its menu and registers with
|
||||
// the panel's watcher - and again whenever the panel restarts (the watcher
|
||||
// owns the icon list, a restart loses it). `icon` is ARGB32 in network byte
|
||||
// order, which is the layout the protocol asks for.
|
||||
void start(std::vector<unsigned char> icon, int width, int height, Callback onShow,
|
||||
Callback onQuit);
|
||||
|
||||
// True once a panel hosts the icon, i.e. the icon is really on screen. Only
|
||||
// then hiding the window is safe: without a tray there is no way back.
|
||||
bool ready() const { return registered_; }
|
||||
|
||||
// The panel mirrors the menu once, so it has to be told when the texts in it
|
||||
// changed - a language switch. Nothing else about the icon changes.
|
||||
void refresh_labels();
|
||||
private:
|
||||
static void on_bus_acquired(GDBusConnection* connection, const gchar* name, gpointer userData);
|
||||
static void on_name_lost(GDBusConnection* connection, const gchar* name, gpointer userData);
|
||||
static void on_watcher_appeared(GDBusConnection* connection, const gchar* name,
|
||||
const gchar* nameOwner, gpointer userData);
|
||||
static void on_watcher_vanished(GDBusConnection* connection, const gchar* name,
|
||||
gpointer userData);
|
||||
static void on_register_reply(GObject* source, GAsyncResult* result, gpointer userData);
|
||||
|
||||
static void on_item_method_call(GDBusConnection* connection, const gchar* sender,
|
||||
const gchar* objectPath, const gchar* interfaceName,
|
||||
const gchar* methodName, GVariant* parameters,
|
||||
GDBusMethodInvocation* invocation, gpointer userData);
|
||||
static GVariant* on_item_get_property(GDBusConnection* connection, const gchar* sender,
|
||||
const gchar* objectPath, const gchar* interfaceName,
|
||||
const gchar* propertyName, GError** error,
|
||||
gpointer userData);
|
||||
static void on_menu_method_call(GDBusConnection* connection, const gchar* sender,
|
||||
const gchar* objectPath, const gchar* interfaceName,
|
||||
const gchar* methodName, GVariant* parameters,
|
||||
GDBusMethodInvocation* invocation, gpointer userData);
|
||||
static GVariant* on_menu_get_property(GDBusConnection* connection, const gchar* sender,
|
||||
const gchar* objectPath, const gchar* interfaceName,
|
||||
const gchar* propertyName, GError** error,
|
||||
gpointer userData);
|
||||
|
||||
void export_objects(GDBusConnection* connection);
|
||||
// Runs what a click on a menu entry means ("clicked" on Show or Close).
|
||||
void handle_event(int id, const char* eventId);
|
||||
// Notes which of the two watcher names is on the bus and registers there.
|
||||
void set_watcher_available(const char* name, bool available);
|
||||
void register_with_watcher();
|
||||
GVariant* icon_pixmaps() const;
|
||||
GVariant* tooltip() const;
|
||||
GVariant* layout(int parentId, int depth) const;
|
||||
GVariant* node(int id, int depth) const;
|
||||
GVariant* group_properties(GVariant* ids) const;
|
||||
GVariant* menu_property(int id, const char* name) const;
|
||||
|
||||
GDBusConnection* connection_ = nullptr;
|
||||
guint itemRegistration_ = 0;
|
||||
guint menuRegistration_ = 0;
|
||||
guint ownerId_ = 0;
|
||||
guint watcherId_ = 0;
|
||||
guint watcherIdX_ = 0;
|
||||
std::string serviceName_;
|
||||
std::vector<unsigned char> icon_;
|
||||
int iconWidth_ = 0;
|
||||
int iconHeight_ = 0;
|
||||
bool watcher_ = false;
|
||||
bool watcherAlias_ = false;
|
||||
bool registering_ = false;
|
||||
bool registered_ = false;
|
||||
const char* registeringWith_ = nullptr;
|
||||
std::string registeredWith_;
|
||||
// Bumped whenever the menu texts change; the panel compares it to know that
|
||||
// what it mirrored is stale.
|
||||
guint menuRevision_ = 1;
|
||||
Callback onShow_;
|
||||
Callback onQuit_;
|
||||
};
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_TRAYICON_H
|
||||
Reference in New Issue
Block a user