/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #ifdef G_OS_UNIX #include #endif #include "zero-panel.h" #define GETTEXT_PACKAGE "zero" #define ZERO_PANEL_GTK_APP_ID "com.emacsos.zero.Panel1.ZeroPanel" #define ZERO_PANEL_SYSTEM_DIR "/usr/share/zero-panel/" // ZERO_PANEL_SYSTEM_DIR must end with slash static const gint WINDOW_WIDTH = 120; static const gint WINDOW_HEIGHT_MIN = 50; static const gchar *DEFAULT_THEME = "zero-panel"; struct _AppData { GtkApplication *app; const gchar *theme_name; gboolean horizontal; GtkWidget *window; GtkWidget *preedit_label; GtkWidget *candidate_label; GtkWidget *prev_page_label; GtkWidget *next_page_label; guint owner_id; ZeroPanel *interface; }; typedef struct _AppData AppData; typedef struct { AppData *appdata; gint move_x; gint move_y; } MoveWindowData; static void update_pagination_labels(AppData *appdata, GVariant *hints) { if (! hints) return; gboolean rb = FALSE; gboolean has_previous_page = FALSE; gboolean has_next_page = FALSE; rb = g_variant_lookup(hints, "has_previous_page", "b", &has_previous_page); if (rb) { if (has_previous_page) { gtk_widget_show(appdata->prev_page_label); } else { gtk_widget_hide(appdata->prev_page_label); } } rb = g_variant_lookup(hints, "has_next_page", "b", &has_next_page); if (rb) { if (has_next_page) { gtk_widget_show(appdata->next_page_label); } else { gtk_widget_hide(appdata->next_page_label); } } } static void move_window(gint x, gint y, AppData *appdata); static gint move_window_on_timeout(gpointer user_data) { MoveWindowData *data = (MoveWindowData *) user_data; move_window(data->move_x, data->move_y, data->appdata); return G_SOURCE_REMOVE; } /** * if move_x and move_y are set in hints, move window to given position. */ static void move_window_maybe(AppData *appdata, GVariant *hints) { if (! hints) return; gint move_x = 0; gint move_y = 0; if (g_variant_lookup(hints, "move_x", "i", &move_x) && g_variant_lookup(hints, "move_y", "i", &move_y)) { /* because right after calling ShowCandidates(), the window * size will likely to change, we use a delay to let WM have * time to resize the window so that gtk can report correct * window size after candidate is updated. based on local * test, 30ms is enough for WM to do the work. If you call * move_window() directly here, the calculated window size is * last known size, which is usually out-dated thus wrong. */ MoveWindowData *data = NULL; static const guint MOVE_WINDOW_DELAY_MS = 30; data = g_new0(MoveWindowData, 1); data->appdata = appdata; data->move_x = move_x; data->move_y = move_y; g_timeout_add_full(G_PRIORITY_DEFAULT, MOVE_WINDOW_DELAY_MS, move_window_on_timeout, data, g_free); } } /** * show preedit_str and candidates in GUI. */ static void show_candidates(AppData *appdata, const gchar *preedit_str, const guint candidate_count, const gchar *const *candidates, GVariant *hints) { GString *candidate_str = NULL; guint i = 0; g_debug("show_candidates()"); gtk_window_resize(GTK_WINDOW(appdata->window), WINDOW_WIDTH, WINDOW_HEIGHT_MIN); gtk_label_set_label(GTK_LABEL(appdata->preedit_label), preedit_str); if (candidate_count > 0) { const gchar *fmt; const gchar *fmt_last; if (appdata->horizontal) { fmt = "%d.%s "; fmt_last = "%d.%s"; } else { fmt = "%d. %s\n"; fmt_last = "%d. %s"; } candidate_str = g_string_new(NULL); for (i = 0; i < candidate_count - 1; ++i) { g_string_append_printf( candidate_str, fmt, i + 1, candidates[i]); } g_string_append_printf( candidate_str, fmt_last, candidate_count == 10 ? 0 : candidate_count, candidates[candidate_count - 1]); gtk_label_set_label(GTK_LABEL(appdata->candidate_label), candidate_str->str); g_string_free(candidate_str, TRUE); } else { gtk_label_set_label(GTK_LABEL(appdata->candidate_label), ""); } update_pagination_labels(appdata, hints); gtk_widget_show(appdata->window); move_window_maybe(appdata, hints); } /** * handle dbus ShowCandidates method */ static gboolean on_handle_show_candidates(ZeroPanel *object, GDBusMethodInvocation *invocation, const gchar *preedit_str, guint candidate_count, const gchar *const *candidates, GVariant *hints, AppData *appdata) { g_debug("on_handle_show_candidates() preedit_str=%s", preedit_str); show_candidates(appdata, preedit_str, candidate_count, candidates, hints); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static gboolean on_handle_hide(ZeroPanel *object, GDBusMethodInvocation *invocation, AppData *appdata) { gtk_widget_hide(appdata->window); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } /** * move window to given coordinate. on best effort. */ static void move_window(gint x, gint y, AppData *appdata) { /* user passed in coordinate is honored on best effort. */ /* do some calculation, try not to let window flow out of screen. */ GdkDisplay *display = NULL; GdkMonitor *monitor = NULL; GdkRectangle geometry = {0}; #ifdef DEBUG int n; #endif g_debug("move_window x=%d y=%d", x, y); /* get screen size */ display = gdk_display_get_default(); #ifdef DEBUG n = gdk_display_get_n_monitors(display); g_debug("%d monitors belong to %s", n, gdk_display_get_name(display)); #endif monitor = gdk_display_get_monitor_at_point(display, x, y); gdk_monitor_get_geometry(monitor, &geometry); /* now monitor size is in geometry.[width|height] */ g_debug("monitor size: x=%d y=%d width=%d height=%d", geometry.x, geometry.y, geometry.width, geometry.height); /* this window's size */ gint width = 0; gint height = 0; gtk_window_get_size(GTK_WINDOW(appdata->window), &width, &height); g_debug("window size:width=%d height=%d", width, height); /* try not to let window flow out of monitor. */ gint newx = x; gint newy = y; if (x - geometry.x + width > geometry.width) { newx = geometry.x + geometry.width - width; g_debug("set newx=%d+%d-%d=%d", geometry.x, geometry.width, width, newx); } if (y - geometry.y + height > geometry.height) { newy = geometry.y + geometry.height - height; g_debug("set newy=%d+%d-%d=%d", geometry.y, geometry.height, height, newy); } if (x < geometry.x) { newx = geometry.x; g_debug("set newx=geometry.x=%d", geometry.x); } if (y < geometry.y) { newy = geometry.y; g_debug("set newy=geometry.y=%d", geometry.y); } gtk_window_move(GTK_WINDOW(appdata->window), newx, newy); } static gboolean on_handle_move(ZeroPanel *object, GDBusMethodInvocation *invocation, gint x, gint y, AppData *appdata) { move_window(x, y, appdata); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static gboolean on_handle_quit(ZeroPanel *object, GDBusMethodInvocation *invocation, AppData *appdata) { gtk_widget_destroy(appdata->window); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static gboolean on_handle_show(ZeroPanel *object, GDBusMethodInvocation *invocation, AppData *appdata) { gtk_widget_show(appdata->window); g_dbus_method_invocation_return_value(invocation, NULL); return TRUE; } static void on_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { AppData *appdata = (AppData *) user_data; GError *err = NULL; g_message("on_bus_acquired() name=%s", name); appdata->interface = zero_panel_skeleton_new(); g_signal_connect(appdata->interface, "handle-show-candidates", G_CALLBACK(on_handle_show_candidates), appdata); g_signal_connect(appdata->interface, "handle-move", G_CALLBACK(on_handle_move), appdata); g_signal_connect(appdata->interface, "handle-show", G_CALLBACK(on_handle_show), appdata); g_signal_connect(appdata->interface, "handle-hide", G_CALLBACK(on_handle_hide), appdata); g_signal_connect(appdata->interface, "handle-quit", G_CALLBACK(on_handle_quit), appdata); g_dbus_interface_skeleton_export( G_DBUS_INTERFACE_SKELETON(appdata->interface), connection, ZERO_PANEL_OBJECT_PATH, &err); if (err) { g_warning("export interface at %s failed: %s", ZERO_PANEL_OBJECT_PATH, err->message); g_error_free(err); g_application_quit(G_APPLICATION(appdata->app)); return; } g_message("interface exported at %s", ZERO_PANEL_OBJECT_PATH); } static void on_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { g_message("on_name_acquired() name=%s", name); } static void on_name_lost(GDBusConnection *connection, const gchar *name, gpointer user_data) { AppData *appdata = (AppData *) user_data; g_warning("on_name_lost() name=%s, exiting now", name); g_application_quit(G_APPLICATION(appdata->app)); } static void config_dbus_service(AppData *appdata) { g_debug("zero-panel config_dbus_service()"); appdata->owner_id = g_bus_own_name( G_BUS_TYPE_SESSION, ZERO_PANEL_WELL_KNOWN_NAME, G_BUS_NAME_OWNER_FLAGS_NONE, on_bus_acquired, on_name_acquired, on_name_lost, appdata, NULL); g_assert_cmpint(appdata->owner_id, >, 0); } static void show_candidates_demo(AppData *appdata) { static const gchar *preedit_str = "p"; static const gchar *candidates_demo[] = {"abc", "def", "ghi", "中文", "中华民族"}; GVariant *hints = NULL; GVariantBuilder *b = NULL; b = g_variant_builder_new(G_VARIANT_TYPE_VARDICT); g_variant_builder_add( b, "{sv}", "has_previous_page", g_variant_new_boolean(TRUE)); g_variant_builder_add( b, "{sv}", "has_next_page", g_variant_new_boolean(TRUE)); hints = g_variant_builder_end(b); show_candidates(appdata, preedit_str, G_N_ELEMENTS(candidates_demo), candidates_demo, hints); g_variant_builder_unref(b); g_variant_unref(hints); } /** * load ui xml file from conventional places, store result in builder. * if load failed, *builder will be set to NULL. * * user should g_object_unref() on builder after use. * * returns: TRUE on success, FALSE on failure. */ static gboolean load_ui_file(AppData *appdata, GtkBuilder **builder) { gchar *filename = NULL; GError *err = NULL; GtkBuilder *result = NULL; g_assert(builder != NULL); g_return_val_if_fail(builder != NULL, FALSE); *builder = NULL; result = gtk_builder_new(); filename = g_strdup_printf("themes/%s.ui", appdata->theme_name); g_debug("loading %s", filename); gtk_builder_add_from_file(result, filename, &err); if (err) { GFile *file = NULL; file = g_file_new_for_path(filename); if (g_file_query_exists(file, NULL)) { g_warning("Error loading %s: %s\n", filename, err->message); } g_object_unref(file); g_clear_error(&err); g_free(filename); g_clear_object(&result); result = gtk_builder_new(); filename = g_strdup_printf("%s%s.ui", ZERO_PANEL_SYSTEM_DIR, appdata->theme_name); g_debug("loading %s", filename); gtk_builder_add_from_file(result, filename, &err); if (err) { g_printerr("Error loading file: %s\n", err->message); g_clear_error(&err); g_clear_object(&result); g_free(filename); return FALSE; } g_message("loaded ui file: %s", filename); } else { g_message("loaded ui file: %s", filename); } g_free(filename); *builder = result; return TRUE; } /** * load and apply css file to this app. * if load failed, just print warning and continue. */ static void load_css_file(AppData *appdata) { GtkCssProvider *provider = NULL; gboolean css_loaded = FALSE; GError *err = NULL; gchar *filename = NULL; provider = gtk_css_provider_new(); filename = g_strdup_printf("themes/%s.css", appdata->theme_name); g_debug("loading %s", filename); gtk_css_provider_load_from_path(provider, filename, &err); if (err) { GFile *file = NULL; file = g_file_new_for_path(filename); if (g_file_query_exists(file, NULL)) { g_warning("Error loading %s: %s\n", filename, err->message); } g_object_unref(file); g_clear_error(&err); g_free(filename); filename = g_strdup_printf("%s%s.css", ZERO_PANEL_SYSTEM_DIR, appdata->theme_name); g_debug("loading %s", filename); gtk_css_provider_load_from_path(provider, filename, &err); if (err) { g_warning("Error loading %s: %s\n", filename, err->message); g_clear_error(&err); } else { g_message("loaded css file: %s", filename); css_loaded = TRUE; } /* css is optional, continue even if loading failed */ } else { g_message("loaded css file: %s", filename); css_loaded = TRUE; } g_free(filename); if (css_loaded) { GdkScreen *screen = NULL; screen = gdk_screen_get_default(); gtk_style_context_add_provider_for_screen( screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_message("css file applied"); } else { g_warning("load css file failed, but continue anyway"); } g_object_unref(provider); } static void create_window(AppData *appdata) { GtkBuilder *builder = NULL; gboolean rb = FALSE; g_message("zero-panel create_window()"); rb = load_ui_file(appdata, &builder); if (!rb) { g_application_quit(G_APPLICATION(appdata->app)); return; } appdata->window = (GtkWidget *) gtk_builder_get_object(builder, "window"); appdata->preedit_label = (GtkWidget *) gtk_builder_get_object(builder, "preedit_label"); appdata->candidate_label = (GtkWidget *) gtk_builder_get_object(builder, "candidate_label"); appdata->prev_page_label = (GtkWidget *) gtk_builder_get_object(builder, "prev_page_label"); appdata->next_page_label = (GtkWidget *) gtk_builder_get_object(builder, "next_page_label"); g_object_unref(builder); if (!(appdata->window && appdata->preedit_label && appdata->candidate_label && appdata->prev_page_label && appdata->next_page_label)) { g_printerr("Some required UI element is not defined in xml\n"); g_application_quit(G_APPLICATION(appdata->app)); return; } g_object_set(G_OBJECT(appdata->window), "application", appdata->app, NULL); gtk_window_set_keep_above(GTK_WINDOW(appdata->window), TRUE); load_css_file(appdata); } static void on_activate(GtkApplication *app, AppData *appdata) { g_message("zero-panel activate"); } #ifdef G_OS_UNIX /** * handle SIGTERM gracefully. */ static gboolean on_sigterm_received(gpointer user_data) { AppData *appdata = (AppData *) user_data; gtk_widget_destroy(appdata->window); return G_SOURCE_REMOVE; } /** * allow graceful shutdown by Ctrl-C and SIGTERM. */ static void setup_sigint_sigterm_handler(AppData *appdata) { GSource *source = NULL; source = g_unix_signal_source_new(SIGTERM); g_source_set_callback(source, on_sigterm_received, appdata, NULL); g_source_attach(source, NULL); g_source_unref(source); source = g_unix_signal_source_new(SIGINT); g_source_set_callback(source, on_sigterm_received, appdata, NULL); g_source_attach(source, NULL); g_source_unref(source); } #endif static void on_startup(GtkApplication *app, AppData *appdata) { g_message("zero-panel startup"); create_window(appdata); config_dbus_service(appdata); #ifdef G_OS_UNIX setup_sigint_sigterm_handler(appdata); #endif /* only show demo if env var DEBUG_ZERO_PANEL=1 */ const gchar *debug; debug = g_getenv("DEBUG_ZERO_PANEL"); if (debug != NULL && g_str_equal(debug, "1")) { show_candidates_demo(appdata); } } static void on_shutdown(GApplication *app, AppData *appdata) { g_message("zero-panel shutdown"); if (appdata->interface) { g_object_unref(appdata->interface); appdata->interface = NULL; } if (appdata->owner_id > 0) { g_bus_unown_name(appdata->owner_id); appdata->owner_id = 0; } } /** * parse command line arguments. * store result in appdata->theme_name and appdata->horizontal. * exit with non-zero if argument parse failed. */ static void parse_command_line_args(int argc, char *argv[], AppData *appdata) { gchar *theme_name = NULL; gboolean horizontal = FALSE; GOptionEntry entries[] = { {"theme", 't', 0, G_OPTION_ARG_STRING, &theme_name, "which theme to use", NULL}, {"horizontal", 'h', 0, G_OPTION_ARG_NONE, &horizontal, "use horizontal mode", NULL}, {NULL} }; GError *err = NULL; GOptionContext *context = NULL; context = g_option_context_new("- run panel for zero input method"); g_option_context_add_main_entries(context, entries, GETTEXT_PACKAGE); g_option_context_add_group(context, gtk_get_option_group(TRUE)); g_option_context_parse(context, &argc, &argv, &err); if (err) { g_print("option parsing failed: %s\n", err->message); g_clear_error(&err); exit(EXIT_FAILURE); } if (theme_name == NULL) { appdata->theme_name = DEFAULT_THEME; } else { appdata->theme_name = theme_name; } g_message("using theme %s", appdata->theme_name); appdata->horizontal = horizontal; g_option_context_free(context); } /** * build zero-panel based on gdbus-codegen generated code. * This application implements dbus com.emacsos.zero.Panel1.PanelInterface, * see the interface xml file for interface method document. */ int main(int argc, char *argv[]) { static AppData appdata = {0}; GtkApplication *app = NULL; int status = 0; // use current system locale instead of C locale. setlocale(LC_ALL, ""); parse_command_line_args(argc, argv, &appdata); app = gtk_application_new(ZERO_PANEL_GTK_APP_ID, 0); g_assert_nonnull(app); appdata.app = app; g_signal_connect(app, "startup", G_CALLBACK(on_startup), &appdata); g_signal_connect(app, "activate", G_CALLBACK(on_activate), &appdata); g_signal_connect(app, "shutdown", G_CALLBACK(on_shutdown), &appdata); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }