Embedd assets icons
This commit is contained in:
191
src/daemonclient.cpp
Normal file
191
src/daemonclient.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
// gKeyMapper - talk to gkeymapper-daemon over the session bus.
|
||||
|
||||
#include "daemonclient.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "ipc_ids.h"
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
|
||||
// The daemon answers within one loop iteration, but it may have to be started
|
||||
// first (D-Bus activation), so allow for a slow first reply.
|
||||
constexpr int kCallTimeoutMs = 5000;
|
||||
|
||||
constexpr const char* kDBusName = "org.freedesktop.DBus";
|
||||
constexpr const char* kDBusPath = "/org/freedesktop/DBus";
|
||||
|
||||
} // namespace
|
||||
|
||||
DaemonClient::~DaemonClient() {
|
||||
if (connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (signal_subscription_ != 0) {
|
||||
g_dbus_connection_signal_unsubscribe(connection_, signal_subscription_);
|
||||
}
|
||||
if (name_subscription_ != 0) {
|
||||
g_dbus_connection_signal_unsubscribe(connection_, name_subscription_);
|
||||
}
|
||||
g_object_unref(connection_);
|
||||
}
|
||||
|
||||
void DaemonClient::init(StatusFn onStatus, KeyFn onKey) {
|
||||
onStatus_ = std::move(onStatus);
|
||||
onKey_ = std::move(onKey);
|
||||
if (started_) {
|
||||
return; // a second activation reuses the existing connection
|
||||
}
|
||||
started_ = true;
|
||||
g_bus_get(G_BUS_TYPE_SESSION, nullptr, &DaemonClient::on_bus_ready, this);
|
||||
}
|
||||
|
||||
void DaemonClient::on_bus_ready(GObject* /*source*/, GAsyncResult* result, gpointer userData) {
|
||||
auto* self = static_cast<DaemonClient*>(userData);
|
||||
|
||||
GError* error = nullptr;
|
||||
GDBusConnection* connection = g_bus_get_finish(result, &error);
|
||||
if (connection == nullptr) {
|
||||
// No session bus: the GUI still edits and saves the file, it just
|
||||
// cannot show the daemon state.
|
||||
std::fprintf(stderr, "gkeymapper: no session bus: %s\n",
|
||||
error != nullptr ? error->message : "unknown error");
|
||||
g_clear_error(&error);
|
||||
self->report_unreachable();
|
||||
return;
|
||||
}
|
||||
|
||||
self->connection_ = connection;
|
||||
self->watch_signals();
|
||||
self->fetch_status();
|
||||
}
|
||||
|
||||
void DaemonClient::watch_signals() {
|
||||
signal_subscription_ = g_dbus_connection_signal_subscribe(
|
||||
connection_, ipc::kBusName, ipc::kInterface, nullptr, ipc::kObjectPath, nullptr,
|
||||
G_DBUS_SIGNAL_FLAGS_NONE, &DaemonClient::on_daemon_signal, this, nullptr);
|
||||
|
||||
// Lets the footer follow the daemon: it flips to "not reachable" when it
|
||||
// stops and back when it (or D-Bus activation) starts it again.
|
||||
name_subscription_ = g_dbus_connection_signal_subscribe(
|
||||
connection_, kDBusName, kDBusName, "NameOwnerChanged", kDBusPath, ipc::kBusName,
|
||||
G_DBUS_SIGNAL_FLAGS_NONE, &DaemonClient::on_name_owner_changed, this, nullptr);
|
||||
}
|
||||
|
||||
void DaemonClient::on_daemon_signal(GDBusConnection* /*connection*/, const gchar* /*sender*/,
|
||||
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
|
||||
const gchar* signalName, GVariant* parameters,
|
||||
gpointer userData) {
|
||||
auto* self = static_cast<DaemonClient*>(userData);
|
||||
|
||||
if (g_strcmp0(signalName, "KeyPressed") == 0) {
|
||||
guint32 index = 0;
|
||||
const gchar* action = nullptr;
|
||||
g_variant_get(parameters, "(u&s)", &index, &action);
|
||||
if (self->onKey_) {
|
||||
self->onKey_(static_cast<int>(index), action != nullptr ? action : "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ConfigReloaded and DeviceChanged carry no state the footer does not
|
||||
// already get from GetStatus, so just refresh it.
|
||||
self->fetch_status();
|
||||
}
|
||||
|
||||
void DaemonClient::on_name_owner_changed(GDBusConnection* /*connection*/, const gchar* /*sender*/,
|
||||
const gchar* /*objectPath*/,
|
||||
const gchar* /*interfaceName*/,
|
||||
const gchar* /*signalName*/, GVariant* parameters,
|
||||
gpointer userData) {
|
||||
auto* self = static_cast<DaemonClient*>(userData);
|
||||
|
||||
const gchar* name = nullptr;
|
||||
const gchar* oldOwner = nullptr;
|
||||
const gchar* newOwner = nullptr;
|
||||
g_variant_get(parameters, "(&s&s&s)", &name, &oldOwner, &newOwner);
|
||||
|
||||
if (newOwner == nullptr || *newOwner == '\0') {
|
||||
self->report_unreachable();
|
||||
} else {
|
||||
self->fetch_status();
|
||||
}
|
||||
}
|
||||
|
||||
void DaemonClient::fetch_status() {
|
||||
if (connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// A call to a name without an owner makes the bus activate the service, so
|
||||
// this also brings the daemon up on demand.
|
||||
g_dbus_connection_call(connection_, ipc::kBusName, ipc::kObjectPath, ipc::kInterface,
|
||||
"GetStatus", nullptr, G_VARIANT_TYPE("(sbss)"),
|
||||
G_DBUS_CALL_FLAGS_NONE, kCallTimeoutMs, nullptr,
|
||||
&DaemonClient::on_status_reply, this);
|
||||
}
|
||||
|
||||
void DaemonClient::on_status_reply(GObject* /*source*/, GAsyncResult* result, gpointer userData) {
|
||||
auto* self = static_cast<DaemonClient*>(userData);
|
||||
|
||||
GError* error = nullptr;
|
||||
GVariant* reply = g_dbus_connection_call_finish(self->connection_, result, &error);
|
||||
if (reply == nullptr) {
|
||||
// Normal when the daemon is not installed or not running.
|
||||
g_clear_error(&error);
|
||||
self->report_unreachable();
|
||||
return;
|
||||
}
|
||||
|
||||
const gchar* version = nullptr;
|
||||
const gchar* device = nullptr;
|
||||
const gchar* configPath = nullptr;
|
||||
gboolean running = FALSE;
|
||||
g_variant_get(reply, "(&sb&s&s)", &version, &running, &device, &configPath);
|
||||
g_variant_unref(reply);
|
||||
|
||||
DaemonStatus status;
|
||||
status.reachable = true;
|
||||
status.version = version != nullptr ? version : "";
|
||||
status.device = device != nullptr ? device : "";
|
||||
status.configPath = configPath != nullptr ? configPath : "";
|
||||
self->publish(status);
|
||||
}
|
||||
|
||||
void DaemonClient::reload() {
|
||||
if (connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
g_dbus_connection_call(connection_, ipc::kBusName, ipc::kObjectPath, ipc::kInterface,
|
||||
"Reload", nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, kCallTimeoutMs,
|
||||
nullptr, &DaemonClient::on_reload_reply, this);
|
||||
}
|
||||
|
||||
void DaemonClient::on_reload_reply(GObject* /*source*/, GAsyncResult* result, gpointer userData) {
|
||||
auto* self = static_cast<DaemonClient*>(userData);
|
||||
|
||||
GError* error = nullptr;
|
||||
GVariant* reply = g_dbus_connection_call_finish(self->connection_, result, &error);
|
||||
if (reply != nullptr) {
|
||||
g_variant_unref(reply);
|
||||
}
|
||||
if (error != nullptr) {
|
||||
// The file watch applies the save anyway, so a missing daemon is fine.
|
||||
g_clear_error(&error);
|
||||
}
|
||||
}
|
||||
|
||||
void DaemonClient::report_unreachable() {
|
||||
DaemonStatus status;
|
||||
status.reachable = false;
|
||||
publish(status);
|
||||
}
|
||||
|
||||
void DaemonClient::publish(const DaemonStatus& status) {
|
||||
if (onStatus_) {
|
||||
onStatus_(status);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
69
src/daemonclient.h
Normal file
69
src/daemonclient.h
Normal file
@@ -0,0 +1,69 @@
|
||||
// gKeyMapper - talk to gkeymapper-daemon over the session bus.
|
||||
#ifndef GKEYMAPPER_DAEMONCLIENT_H
|
||||
#define GKEYMAPPER_DAEMONCLIENT_H
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// What the footer needs to know about the daemon.
|
||||
struct DaemonStatus {
|
||||
bool reachable = false;
|
||||
std::string version;
|
||||
std::string device;
|
||||
std::string configPath;
|
||||
};
|
||||
|
||||
// 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
|
||||
// travel through here.
|
||||
class DaemonClient {
|
||||
public:
|
||||
using StatusFn = std::function<void(const DaemonStatus&)>;
|
||||
using KeyFn = std::function<void(int index, const std::string& action)>;
|
||||
|
||||
DaemonClient() = default;
|
||||
~DaemonClient();
|
||||
|
||||
DaemonClient(const DaemonClient&) = delete;
|
||||
DaemonClient& operator=(const DaemonClient&) = delete;
|
||||
|
||||
void init(StatusFn onStatus, KeyFn onKey);
|
||||
|
||||
// Best effort: asks the daemon to re-read the mappings file. Its file watch
|
||||
// would pick the save up by itself, this only makes it immediate.
|
||||
void reload();
|
||||
|
||||
private:
|
||||
static void on_bus_ready(GObject* source, GAsyncResult* result, gpointer userData);
|
||||
static void on_daemon_signal(GDBusConnection* connection, const gchar* sender,
|
||||
const gchar* objectPath, const gchar* interfaceName,
|
||||
const gchar* signalName, GVariant* parameters,
|
||||
gpointer userData);
|
||||
static void on_name_owner_changed(GDBusConnection* connection, const gchar* sender,
|
||||
const gchar* objectPath, const gchar* interfaceName,
|
||||
const gchar* signalName, GVariant* parameters,
|
||||
gpointer userData);
|
||||
static void on_status_reply(GObject* source, GAsyncResult* result, gpointer userData);
|
||||
static void on_reload_reply(GObject* source, GAsyncResult* result, gpointer userData);
|
||||
|
||||
void watch_signals();
|
||||
void fetch_status();
|
||||
void report_unreachable();
|
||||
void publish(const DaemonStatus& status);
|
||||
|
||||
GDBusConnection* connection_ = nullptr;
|
||||
guint signal_subscription_ = 0;
|
||||
guint name_subscription_ = 0;
|
||||
bool started_ = false;
|
||||
StatusFn onStatus_;
|
||||
KeyFn onKey_;
|
||||
};
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_DAEMONCLIENT_H
|
||||
155
src/main.cpp
155
src/main.cpp
@@ -10,6 +10,8 @@
|
||||
#include <vector>
|
||||
|
||||
#include "capturedialog.h"
|
||||
#include "daemonclient.h"
|
||||
#include "ipc_ids.h"
|
||||
#include "keyrow.h"
|
||||
#include "mapping.h"
|
||||
#include "mappings.h"
|
||||
@@ -29,6 +31,59 @@ 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;
|
||||
std::string device;
|
||||
std::string lastKey;
|
||||
} daemonFooter;
|
||||
|
||||
GtkWidget* daemonLabel = nullptr;
|
||||
gkeymapper::DaemonClient daemonClient;
|
||||
|
||||
// Rebuilds the footer line from the state above. " · " is a separator, not a
|
||||
// sentence, so it is not translated.
|
||||
static void refresh_footer() {
|
||||
if (daemonLabel == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::string text;
|
||||
if (!daemonFooter.reachable) {
|
||||
text = _("Daemon: not reachable");
|
||||
} else {
|
||||
text = _("Daemon: running");
|
||||
if (!daemonFooter.device.empty()) {
|
||||
g_autofree gchar* device =
|
||||
g_strdup_printf(_("Device: %s"), daemonFooter.device.c_str());
|
||||
text += " · ";
|
||||
text += device;
|
||||
}
|
||||
if (!daemonFooter.lastKey.empty()) {
|
||||
text += " · ";
|
||||
text += daemonFooter.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.device = status.device;
|
||||
if (!status.reachable) {
|
||||
daemonFooter.lastKey.clear(); // stale once the daemon is gone
|
||||
}
|
||||
refresh_footer();
|
||||
}
|
||||
|
||||
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;
|
||||
refresh_footer();
|
||||
}
|
||||
|
||||
static void on_pick_file_response(GtkDialog* dialog, int response, gpointer userData) {
|
||||
if (response == GTK_RESPONSE_ACCEPT) {
|
||||
KeyRow* row = static_cast<KeyRow*>(userData);
|
||||
@@ -101,6 +156,10 @@ void on_save_clicked(GtkButton* /*btn*/, gpointer /*userData*/) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Best effort: the daemon would pick the save up through its file watch
|
||||
// within about half a second, this just applies it right away.
|
||||
daemonClient.reload();
|
||||
|
||||
std::cout << _("Mappings saved to ") << path << "\n";
|
||||
}
|
||||
|
||||
@@ -134,24 +193,50 @@ static void on_import_response(GtkDialog* dialog, int response, gpointer /*userD
|
||||
|
||||
static std::string find_asset(const std::string& name);
|
||||
|
||||
// The icons are compiled into the executable (data/icons.gresource.xml), so the
|
||||
// GUI has them even when no assets are installed. The file lookup stays as a
|
||||
// fallback for a build without the resource; because the resource is embedded,
|
||||
// it wins over any installed file.
|
||||
static const char* kIconResource = "/dev/rager/gkeymapper/icon.png";
|
||||
static const char* kIconThemeResource = "/dev/rager/gkeymapper/icons";
|
||||
|
||||
static bool have_embedded_icon() {
|
||||
return g_resources_get_info(kIconResource, G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr, nullptr,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
static GtkWidget* make_icon_image(int pixelSize) {
|
||||
GtkWidget* image = have_embedded_icon()
|
||||
? gtk_image_new_from_resource(kIconResource)
|
||||
: gtk_image_new_from_file(find_asset("icon.png").c_str());
|
||||
gtk_image_set_pixel_size(GTK_IMAGE(image), pixelSize);
|
||||
return image;
|
||||
}
|
||||
|
||||
static void show_about_dialog(GtkWindow* parent) {
|
||||
GtkWidget* dialog = gtk_about_dialog_new();
|
||||
gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(dialog), _("gKeyMapper"));
|
||||
gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dialog), "0.1.0");
|
||||
gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dialog), gkeymapper::ipc::kVersion);
|
||||
gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(dialog),
|
||||
_("G-Key mapper for the Logitech G910 (GTK4)."));
|
||||
gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(dialog), "https://rager.dev");
|
||||
gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dialog), "Thomas Rager - rager.dev");
|
||||
|
||||
std::string iconPath = find_asset("icon.png");
|
||||
GError* error = nullptr;
|
||||
GdkTexture* texture = gdk_texture_new_from_filename(iconPath.c_str(), &error);
|
||||
GdkTexture* texture = nullptr;
|
||||
if (have_embedded_icon()) {
|
||||
texture = gdk_texture_new_from_resource(kIconResource);
|
||||
} else {
|
||||
std::string iconPath = find_asset("icon.png");
|
||||
GError* error = nullptr;
|
||||
texture = gdk_texture_new_from_filename(iconPath.c_str(), &error);
|
||||
if (!texture && error) {
|
||||
g_printerr("Could not load icon: %s\n", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
}
|
||||
if (texture) {
|
||||
gtk_about_dialog_set_logo(GTK_ABOUT_DIALOG(dialog), GDK_PAINTABLE(texture));
|
||||
g_object_unref(texture);
|
||||
} else if (error) {
|
||||
g_printerr("Could not load icon: %s\n", error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
|
||||
g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL);
|
||||
@@ -236,10 +321,14 @@ void build_ui(GtkApplication* app) {
|
||||
|
||||
GtkWidget* headerBar = gtk_header_bar_new();
|
||||
gtk_window_set_titlebar(GTK_WINDOW(window), headerBar);
|
||||
// Serve "gkeymapper" from the embedded icons, so the window icon resolves
|
||||
// even without an installed hicolor theme.
|
||||
gtk_icon_theme_add_resource_path(
|
||||
gtk_icon_theme_get_for_display(gtk_widget_get_display(window)), kIconThemeResource);
|
||||
gtk_window_set_icon_name(GTK_WINDOW(window), "gkeymapper");
|
||||
|
||||
GtkWidget* titleBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
GtkWidget* iconImage = gtk_image_new_from_file(find_asset("icon.png").c_str());
|
||||
GtkWidget* iconImage = make_icon_image(24);
|
||||
gtk_image_set_pixel_size(GTK_IMAGE(iconImage), 24);
|
||||
gtk_box_append(GTK_BOX(titleBox), iconImage);
|
||||
gtk_box_append(GTK_BOX(titleBox), gtk_label_new(_("gKeyMapper")));
|
||||
@@ -261,23 +350,17 @@ void build_ui(GtkApplication* app) {
|
||||
GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_window_set_child(GTK_WINDOW(window), outer);
|
||||
|
||||
// Title / subtitle
|
||||
GtkWidget* headerBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||
gtk_widget_set_margin_top(headerBox, 16);
|
||||
gtk_widget_set_margin_bottom(headerBox, 8);
|
||||
gtk_widget_set_margin_start(headerBox, 16);
|
||||
gtk_widget_set_margin_end(headerBox, 16);
|
||||
gtk_box_append(GTK_BOX(outer), headerBox);
|
||||
|
||||
GtkWidget* title = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(title), "<span size='x-large' weight='bold'>G-Key Mapper</span>");
|
||||
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
|
||||
gtk_box_append(GTK_BOX(headerBox), title);
|
||||
|
||||
GtkWidget* subtitle = gtk_label_new(_("G1 to G9 of the Logitech G910 remapped to keys or commands"));
|
||||
// Subtitle only: the app name is already in the header bar, and the heading
|
||||
// spelled it differently ("G-Key Mapper") on top of that.
|
||||
GtkWidget* subtitle =
|
||||
gtk_label_new(_("G1 to G9 of the Logitech G910 remapped to keys or commands"));
|
||||
gtk_label_set_xalign(GTK_LABEL(subtitle), 0.0f);
|
||||
gtk_widget_add_css_class(subtitle, "dim-label");
|
||||
gtk_box_append(GTK_BOX(headerBox), subtitle);
|
||||
gtk_widget_set_margin_top(subtitle, 16);
|
||||
gtk_widget_set_margin_bottom(subtitle, 8);
|
||||
gtk_widget_set_margin_start(subtitle, 16);
|
||||
gtk_widget_set_margin_end(subtitle, 16);
|
||||
gtk_box_append(GTK_BOX(outer), subtitle);
|
||||
|
||||
// Table with all nine G keys (no scrolling needed, everything stays visible)
|
||||
GtkWidget* grid = gtk_grid_new();
|
||||
@@ -340,28 +423,44 @@ void build_ui(GtkApplication* app) {
|
||||
update_row_state(&rows[i]);
|
||||
}
|
||||
|
||||
// Footer with save button
|
||||
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
// Footer with save button and the daemon status line
|
||||
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_widget_set_margin_top(footer, 8);
|
||||
gtk_widget_set_margin_bottom(footer, 16);
|
||||
gtk_widget_set_margin_start(footer, 16);
|
||||
gtk_widget_set_margin_end(footer, 16);
|
||||
gtk_box_append(GTK_BOX(outer), footer);
|
||||
|
||||
GtkWidget* footerRow = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_box_append(GTK_BOX(footer), footerRow);
|
||||
|
||||
GtkWidget* saveBtn = gtk_button_new_with_label(_("Save mappings"));
|
||||
// No "suggested-action" class on purpose: it gets a strong accent color
|
||||
// (e.g. Mint-Y green) that clashes with the rest of the layout. The plain
|
||||
// button follows the system theme like everything else.
|
||||
g_signal_connect(saveBtn, "clicked", G_CALLBACK(on_save_clicked), nullptr);
|
||||
gtk_box_append(GTK_BOX(footer), saveBtn);
|
||||
gtk_box_append(GTK_BOX(footerRow), saveBtn);
|
||||
|
||||
GtkWidget* hint = gtk_label_new(_("Saved to ~/.config/gKeyMapper/mappings.conf"));
|
||||
gtk_widget_add_css_class(hint, "dim-label");
|
||||
gtk_widget_set_halign(hint, GTK_ALIGN_END);
|
||||
gtk_widget_set_hexpand(hint, TRUE);
|
||||
gtk_box_append(GTK_BOX(footer), hint);
|
||||
gtk_box_append(GTK_BOX(footerRow), hint);
|
||||
|
||||
// Daemon status: control plane only, the keypress path never leaves the
|
||||
// daemon. Text starts neutral so the footer does not change height when the
|
||||
// first reply arrives. Plain label on purpose: GTK4's GtkStatusbar would add
|
||||
// a message stack (pop/push) and is exposed over AT-SPI as a generic filler,
|
||||
// so it brings no benefit here.
|
||||
daemonLabel = gtk_label_new(_("Daemon: checking..."));
|
||||
gtk_widget_add_css_class(daemonLabel, "dim-label");
|
||||
gtk_label_set_xalign(GTK_LABEL(daemonLabel), 0.0f);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(daemonLabel), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(footer), daemonLabel);
|
||||
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
|
||||
daemonClient.init(on_daemon_status, on_daemon_key);
|
||||
}
|
||||
|
||||
static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
|
||||
@@ -392,7 +491,7 @@ int main(int argc, char** argv) {
|
||||
bind_textdomain_codeset(PACKAGE, "UTF-8");
|
||||
textdomain(PACKAGE);
|
||||
|
||||
GtkApplication* app = gtk_application_new("com.example.gkeymapper",
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user