/* X-Chat * Copyright (C) 1998 Peter Zelezny. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include "fe-gtk.h" #ifdef GDK_WINDOWING_WIN32 #include #endif #ifdef WIN32 #include #include #else #include #endif #include "../common/zoitechat.h" #include "../common/fe.h" #include "../common/util.h" #include "../common/text.h" #include "../common/cfgfiles.h" #include "../common/zoitechatc.h" #include "../common/plugin.h" #include "../common/server.h" #include "../common/url.h" #include "gtkutil.h" #include "maingui.h" #include "pixmaps.h" #include "chanlist.h" #include "joind.h" #include "xtext.h" #include "palette.h" #include "menu.h" #include "notifygui.h" #include "textgui.h" #include "fkeys.h" #include "plugin-tray.h" #include "urlgrab.h" #include "setup.h" #include "plugin-notification.h" #ifdef USE_LIBCANBERRA #include #endif cairo_surface_t *channelwin_pix; #ifdef USE_LIBCANBERRA static ca_context *ca_con; #endif #ifdef HAVE_GTK_MAC GtkosxApplication *osx_app; #endif /* === command-line parameter parsing : requires glib 2.6 === */ static char *arg_cfgdir = NULL; static gint arg_show_autoload = 0; static gint arg_show_config = 0; static gint arg_show_version = 0; static gint arg_minimize = 0; static const GOptionEntry gopt_entries[] = { {"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL}, {"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"}, {"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL}, {"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL}, {"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL}, {"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel?key URL"), "URL"}, {"command", 'c', 0, G_OPTION_ARG_STRING, &arg_command, N_("Execute command:"), "COMMAND"}, #ifdef USE_DBUS {"existing", 'e', 0, G_OPTION_ARG_NONE, &arg_existing, N_("Open URL or execute command in an existing ZoiteChat"), NULL}, #endif {"minimize", 0, 0, G_OPTION_ARG_INT, &arg_minimize, N_("Begin minimized. Level 0=Normal 1=Iconified 2=Tray"), N_("level")}, {"version", 'v', 0, G_OPTION_ARG_NONE, &arg_show_version, N_("Show version information"), NULL}, {G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_STRING_ARRAY, &arg_urls, N_("Open an irc://server:port/channel?key URL"), "URL"}, {NULL} }; #ifdef WIN32 static void create_msg_dialog (gchar *title, gchar *message) { GtkWidget *dialog; dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message); /* Window classes are required for GTK CSS selectors like * .zoitechat-dark / .zoitechat-light. */ fe_apply_theme_to_toplevel (dialog); gtk_window_set_title (GTK_WINDOW (dialog), title); /* On Win32 we automatically have the icon. If we try to load it explicitly, it will look ugly for some reason. */ #ifndef WIN32 pixmaps_init (); gtk_window_set_icon (GTK_WINDOW (dialog), pix_zoitechat); #endif gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); } static char *win32_argv0_dir; static void win32_set_gsettings_schema_dir (void) { char *base_path; char *share_path; char *schema_path; char *xdg_data_dirs; char **xdg_parts; gboolean have_share_path = FALSE; gint i; base_path = g_win32_get_package_installation_directory_of_module (NULL); if (base_path == NULL) return; share_path = g_build_filename (base_path, "share", NULL); /* Ensure GTK can discover bundled icon themes and other shared data. */ xdg_data_dirs = g_strdup (g_getenv ("XDG_DATA_DIRS")); if (xdg_data_dirs && *xdg_data_dirs) { xdg_parts = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1); for (i = 0; xdg_parts[i] != NULL; i++) { if (g_ascii_strcasecmp (xdg_parts[i], share_path) == 0) { have_share_path = TRUE; break; } } g_strfreev (xdg_parts); if (!have_share_path) { char *updated = g_strdup_printf ("%s%c%s", share_path, G_SEARCHPATH_SEPARATOR, xdg_data_dirs); g_setenv ("XDG_DATA_DIRS", updated, TRUE); g_free (updated); } } else { g_setenv ("XDG_DATA_DIRS", share_path, TRUE); } schema_path = g_build_filename (base_path, "share", "glib-2.0", "schemas", NULL); if (g_getenv ("GSETTINGS_SCHEMA_DIR") == NULL && g_file_test (schema_path, G_FILE_TEST_IS_DIR)) g_setenv ("GSETTINGS_SCHEMA_DIR", schema_path, FALSE); g_free (xdg_data_dirs); g_free (share_path); g_free (schema_path); g_free (base_path); } static void win32_configure_pixbuf_loaders (void) { char *base_path; char *pixbuf_root; GDir *versions; const gchar *entry; base_path = g_win32_get_package_installation_directory_of_module (NULL); if (!base_path) return; pixbuf_root = g_build_filename (base_path, "lib", "gdk-pixbuf-2.0", NULL); if (!g_file_test (pixbuf_root, G_FILE_TEST_IS_DIR)) { g_free (pixbuf_root); g_free (base_path); return; } versions = g_dir_open (pixbuf_root, 0, NULL); if (versions) { while ((entry = g_dir_read_name (versions)) != NULL) { char *module_dir = g_build_filename (pixbuf_root, entry, "loaders", NULL); char *module_file = g_build_filename (pixbuf_root, entry, "loaders.cache", NULL); if (g_file_test (module_dir, G_FILE_TEST_IS_DIR)) g_setenv ("GDK_PIXBUF_MODULEDIR", module_dir, TRUE); if (g_file_test (module_file, G_FILE_TEST_EXISTS)) g_setenv ("GDK_PIXBUF_MODULE_FILE", module_file, TRUE); g_free (module_file); g_free (module_dir); if (g_getenv ("GDK_PIXBUF_MODULEDIR") != NULL) break; } g_dir_close (versions); } g_free (pixbuf_root); g_free (base_path); } static void win32_configure_icon_theme (void) { GtkIconTheme *theme; const char *env_icons_path; char *base_path; char *icons_path; char *cwd_dir; char *cwd_path; char *argv0_icons_path; const char *selected_source = NULL; char *selected_path = NULL; #define WIN32_SET_ICON_PATH(source_name, path_value) \ G_STMT_START { \ if ((path_value) != NULL && g_file_test ((path_value), G_FILE_TEST_IS_DIR)) \ { \ gtk_icon_theme_append_search_path (theme, (path_value)); \ if (selected_path == NULL) \ { \ selected_source = (source_name); \ selected_path = g_strdup (path_value); \ } \ } \ } G_STMT_END theme = gtk_icon_theme_get_default (); if (!theme) return; env_icons_path = g_getenv ("ZOITECHAT_ICON_PATH"); if (env_icons_path && *env_icons_path) WIN32_SET_ICON_PATH ("ZOITECHAT_ICON_PATH", env_icons_path); base_path = g_win32_get_package_installation_directory_of_module (NULL); if (base_path) { icons_path = g_build_filename (base_path, "share", "icons", NULL); WIN32_SET_ICON_PATH ("module base", icons_path); g_free (icons_path); } cwd_dir = g_get_current_dir (); cwd_path = g_build_filename (cwd_dir, "share", "icons", NULL); WIN32_SET_ICON_PATH ("current working directory", cwd_path); g_free (cwd_path); g_free (cwd_dir); if (win32_argv0_dir) { argv0_icons_path = g_build_filename (win32_argv0_dir, "share", "icons", NULL); WIN32_SET_ICON_PATH ("argv[0] directory", argv0_icons_path); g_free (argv0_icons_path); } if (selected_path) g_message ("win32_configure_icon_theme: selected icon path (%s): %s", selected_source, selected_path); else g_message ("win32_configure_icon_theme: no usable icon path found (checked ZOITECHAT_ICON_PATH, module base/share/icons, cwd/share/icons, argv[0]/share/icons)"); g_free (selected_path); g_free (base_path); #undef WIN32_SET_ICON_PATH } #endif int fe_args (int argc, char *argv[]) { GError *error = NULL; GOptionContext *context; char *buffer; const char *desktop_id = "net.zoite.Zoitechat"; #ifdef ENABLE_NLS bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); #endif context = g_option_context_new (NULL); #ifdef WIN32 g_option_context_set_help_enabled (context, FALSE); /* disable stdout help as stdout is unavailable for subsystem:windows */ #endif g_option_context_add_main_entries (context, gopt_entries, GETTEXT_PACKAGE); g_option_context_add_group (context, gtk_get_option_group (FALSE)); g_option_context_parse (context, &argc, &argv, &error); #ifdef WIN32 if (error) /* workaround for argv not being available when using subsystem:windows */ { if (error->message) /* the error message contains argv so search for patterns in that */ { if (strstr (error->message, "--help-all") != NULL) { buffer = g_option_context_get_help (context, FALSE, NULL); gtk_init (&argc, &argv); create_msg_dialog ("Long Help", buffer); g_free (buffer); return 0; } else if (strstr (error->message, "--help") != NULL || strstr (error->message, "-?") != NULL) { buffer = g_option_context_get_help (context, TRUE, NULL); gtk_init (&argc, &argv); create_msg_dialog ("Help", buffer); g_free (buffer); return 0; } else { buffer = g_strdup_printf ("%s\n", error->message); gtk_init (&argc, &argv); create_msg_dialog ("Error", buffer); g_free (buffer); return 1; } } } #else if (error) { if (error->message) printf ("%s\n", error->message); return 1; } #endif g_option_context_free (context); if (arg_show_version) { buffer = g_strdup_printf ("%s %s", PACKAGE_NAME, PACKAGE_VERSION); #ifdef WIN32 gtk_init (&argc, &argv); create_msg_dialog ("Version Information", buffer); #else puts (buffer); #endif g_free (buffer); return 0; } if (arg_show_autoload) { buffer = g_strdup_printf ("%s%caddons%c", get_xdir(), G_DIR_SEPARATOR, G_DIR_SEPARATOR); #ifdef WIN32 gtk_init (&argc, &argv); create_msg_dialog ("Plugin/Script Auto-load Directory", buffer); #else puts (buffer); #endif g_free (buffer); return 0; } if (arg_show_config) { buffer = g_strdup_printf ("%s%c", get_xdir(), G_DIR_SEPARATOR); #ifdef WIN32 gtk_init (&argc, &argv); create_msg_dialog ("User Config Directory", buffer); #else puts (buffer); #endif g_free (buffer); return 0; } #ifdef WIN32 win32_set_gsettings_schema_dir (); win32_configure_pixbuf_loaders (); /* this is mainly for irc:// URL handling. When windows calls us from */ /* I.E, it doesn't give an option of "Start in" directory, like short */ /* cuts can. So we have to set the current dir manually, to the path */ /* of the exe. */ { g_free (win32_argv0_dir); win32_argv0_dir = g_path_get_dirname (argv[0]); if (win32_argv0_dir) chdir (win32_argv0_dir); } #endif g_set_prgname (desktop_id); #ifndef WIN32 gdk_set_program_class (desktop_id); #endif gtk_init (&argc, &argv); #ifdef WIN32 win32_configure_icon_theme (); #endif #ifdef HAVE_GTK_MAC osx_app = g_object_new(GTKOSX_TYPE_APPLICATION, NULL); #endif return -1; } const char cursor_color_rc[] = "style \"xc-ib-st\"" "{" "GtkEntry::cursor-color=\"#%02x%02x%02x\"" "}" "widget \"*.zoitechat-inputbox\" style : application \"xc-ib-st\""; InputStyle *create_input_style (InputStyle *style); static const char adwaita_workaround_rc[] = "style \"zoitechat-input-workaround\"" "{" "engine \"pixmap\" {" "image {" "function = FLAT_BOX\n" "state = NORMAL\n" "}" "image {" "function = FLAT_BOX\n" "state = ACTIVE\n" "}" "}" "}" "widget \"*.zoitechat-inputbox\" style \"zoitechat-input-workaround\""; #ifdef G_OS_WIN32 #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif static gboolean fe_win32_high_contrast_is_enabled (void) { HIGHCONTRASTW hc; ZeroMemory (&hc, sizeof (hc)); hc.cbSize = sizeof (hc); if (!SystemParametersInfoW (SPI_GETHIGHCONTRAST, sizeof (hc), &hc, 0)) return FALSE; return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0; } static gboolean fe_win32_try_get_system_dark (gboolean *prefer_dark) { DWORD value = 1; DWORD value_size = sizeof (value); LSTATUS status; status = RegGetValueW (HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, NULL, &value, &value_size); if (status != ERROR_SUCCESS) status = RegGetValueW (HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"SystemUsesLightTheme", RRF_RT_REG_DWORD, NULL, &value, &value_size); if (status != ERROR_SUCCESS) return FALSE; *prefer_dark = (value == 0); return TRUE; } static void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) { HWND hwnd; BOOL use_dark; if (!window || !gtk_widget_get_realized (window)) return; hwnd = gdk_win32_window_get_handle (gtk_widget_get_window (window)); if (!hwnd) return; if (fe_win32_high_contrast_is_enabled ()) return; use_dark = dark_mode ? TRUE : FALSE; DwmSetWindowAttribute (hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &use_dark, sizeof (use_dark)); } #else static void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) { (void) window; (void) dark_mode; } #endif static gboolean fe_system_prefers_dark (void) { GtkSettings *settings = gtk_settings_get_default (); gboolean theme_name_prefers_dark = FALSE; gboolean property_prefers_dark = FALSE; gboolean prefer_dark = FALSE; char *theme_name = NULL; #ifdef G_OS_WIN32 gboolean have_win_pref = FALSE; gboolean win_prefers_dark = FALSE; if (fe_win32_high_contrast_is_enabled ()) return FALSE; #endif if (!settings) return FALSE; g_object_get (settings, "gtk-theme-name", &theme_name, NULL); if (theme_name) { char *lower = g_ascii_strdown (theme_name, -1); if (g_str_has_suffix (lower, "-dark") || g_strrstr (lower, "dark")) theme_name_prefers_dark = TRUE; g_free (lower); g_free (theme_name); } if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme")) { /* Even if we last wrote this property, the toolkit or desktop can update * it later, so AUTO mode should keep reading it as a signal. */ g_object_get (settings, "gtk-application-prefer-dark-theme", &property_prefers_dark, NULL); } #ifdef G_OS_WIN32 have_win_pref = fe_win32_try_get_system_dark (&win_prefers_dark); #endif /* Deterministic precedence: any explicit dark signal wins. */ prefer_dark = theme_name_prefers_dark || property_prefers_dark; #ifdef G_OS_WIN32 prefer_dark = prefer_dark || (have_win_pref && win_prefers_dark); #endif return prefer_dark; } static gboolean auto_dark_mode_enabled = FALSE; static gboolean dark_mode_state_initialized = FALSE; static GtkCssProvider *gtk3_theme_provider = NULL; static char *gtk3_theme_provider_name = NULL; static gboolean gtk3_theme_provider_dark = FALSE; static GResource *gtk3_theme_resource = NULL; static char *gtk3_theme_resource_path = NULL; static void fe_gtk3_theme_unregister_resource (void) { if (gtk3_theme_resource) { g_resources_unregister (gtk3_theme_resource); g_clear_pointer (>k3_theme_resource, g_resource_unref); } g_clear_pointer (>k3_theme_resource_path, g_free); } static gboolean fe_gtk3_theme_register_resource (const char *resource_path, GError **error) { GResource *resource; if (!resource_path || !*resource_path) { fe_gtk3_theme_unregister_resource (); return TRUE; } if (gtk3_theme_resource && gtk3_theme_resource_path && g_strcmp0 (gtk3_theme_resource_path, resource_path) == 0) return TRUE; resource = g_resource_load (resource_path, error); if (!resource) return FALSE; fe_gtk3_theme_unregister_resource (); g_resources_register (resource); gtk3_theme_resource = resource; gtk3_theme_resource_path = g_strdup (resource_path); return TRUE; } gboolean fe_apply_gtk3_theme_with_reload (const char *theme_name, gboolean force_reload, GError **error) { GdkScreen *screen = gdk_screen_get_default (); char *theme_dir = NULL; char *gtk3_dir = NULL; char *gtk_css = NULL; char *gtk_dark_css = NULL; char *gtk_resource = NULL; const char *selected_css = NULL; gboolean dark = fe_dark_mode_is_enabled (); GList *toplevels, *node; if (!theme_name || !*theme_name) { if (gtk3_theme_provider && screen) { gtk_style_context_remove_provider_for_screen ( screen, GTK_STYLE_PROVIDER (gtk3_theme_provider)); gtk_style_context_reset_widgets (screen); } g_clear_object (>k3_theme_provider); g_clear_pointer (>k3_theme_provider_name, g_free); gtk3_theme_provider_dark = FALSE; fe_gtk3_theme_unregister_resource (); toplevels = gtk_window_list_toplevels (); for (node = toplevels; node; node = node->next) fe_apply_theme_to_toplevel (GTK_WIDGET (node->data)); g_list_free (toplevels); return TRUE; } if (!force_reload && gtk3_theme_provider_name && g_strcmp0 (gtk3_theme_provider_name, theme_name) == 0 && gtk3_theme_provider_dark == dark) { return TRUE; } theme_dir = g_build_filename (get_xdir (), "gtk3-themes", theme_name, NULL); gtk3_dir = g_build_filename (theme_dir, "gtk-3.0", NULL); gtk_css = g_build_filename (gtk3_dir, "gtk.css", NULL); gtk_dark_css = g_build_filename (gtk3_dir, "gtk-dark.css", NULL); gtk_resource = g_build_filename (gtk3_dir, "gtk.gresource", NULL); if (!g_file_test (gtk_css, G_FILE_TEST_IS_REGULAR)) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, _("GTK3 theme '%s' is missing gtk-3.0/gtk.css."), theme_name); g_free (gtk_dark_css); g_free (gtk_css); g_free (gtk3_dir); g_free (theme_dir); return FALSE; } if (dark && g_file_test (gtk_dark_css, G_FILE_TEST_IS_REGULAR)) selected_css = gtk_dark_css; else selected_css = gtk_css; if (g_file_test (gtk_resource, G_FILE_TEST_IS_REGULAR)) { if (!fe_gtk3_theme_register_resource (gtk_resource, error)) { g_free (gtk_resource); g_free (gtk_dark_css); g_free (gtk_css); g_free (gtk3_dir); g_free (theme_dir); return FALSE; } } else { fe_gtk3_theme_unregister_resource (); } if (!gtk3_theme_provider) gtk3_theme_provider = gtk_css_provider_new (); if (!gtk_css_provider_load_from_path (gtk3_theme_provider, selected_css, error)) { g_free (gtk_dark_css); g_free (gtk_css); g_free (gtk_resource); g_free (gtk3_dir); g_free (theme_dir); return FALSE; } if (screen) { gtk_style_context_add_provider_for_screen ( screen, GTK_STYLE_PROVIDER (gtk3_theme_provider), GTK_STYLE_PROVIDER_PRIORITY_THEME); gtk_style_context_reset_widgets (screen); } g_free (gtk3_theme_provider_name); gtk3_theme_provider_name = g_strdup (theme_name); gtk3_theme_provider_dark = dark; toplevels = gtk_window_list_toplevels (); for (node = toplevels; node; node = node->next) fe_apply_theme_to_toplevel (GTK_WIDGET (node->data)); g_list_free (toplevels); g_free (gtk_dark_css); g_free (gtk_css); g_free (gtk_resource); g_free (gtk3_dir); g_free (theme_dir); return TRUE; } gboolean fe_apply_gtk3_theme (const char *theme_name, GError **error) { return fe_apply_gtk3_theme_with_reload (theme_name, FALSE, error); } static void fe_set_gtk_prefer_dark_theme (gboolean dark) { GtkSettings *settings = gtk_settings_get_default (); if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme")) { g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL); } } #ifdef G_OS_WIN32 static void fe_append_window_theme_class_css (GString *css, const char *class_name, const PaletteColor *fg, const PaletteColor *bg) { char *fg_css = gdk_rgba_to_string (fg); char *bg_css = gdk_rgba_to_string (bg); g_string_append_printf (css, "window.%s, .%s {" "background-color: %s;" "color: %s;" "}", class_name, class_name, bg_css, fg_css); g_free (fg_css); g_free (bg_css); } static void fe_apply_windows_theme (gboolean dark) { fe_set_gtk_prefer_dark_theme (dark); { static GtkCssProvider *win_theme_provider = NULL; GdkScreen *screen = gdk_screen_get_default (); const PaletteColor *light_palette = palette_user_colors (); const PaletteColor *dark_palette = palette_dark_colors (); GString *css = g_string_new (NULL); if (!win_theme_provider) win_theme_provider = gtk_css_provider_new (); fe_append_window_theme_class_css (css, "zoitechat-dark", &dark_palette[COL_FG], &dark_palette[COL_BG]); fe_append_window_theme_class_css (css, "zoitechat-light", &light_palette[COL_FG], &light_palette[COL_BG]); gtk_css_provider_load_from_data (win_theme_provider, css->str, -1, NULL); g_string_free (css, TRUE); if (screen) gtk_style_context_add_provider_for_screen ( screen, GTK_STYLE_PROVIDER (win_theme_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } } #endif static void fe_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer data) { gboolean enabled; (void) settings; (void) pspec; (void) data; if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO) return; enabled = fe_system_prefers_dark (); if (enabled == auto_dark_mode_enabled) return; auto_dark_mode_enabled = enabled; fe_apply_theme_for_mode (ZOITECHAT_DARK_MODE_AUTO, NULL); setup_apply_real (0, TRUE, FALSE, FALSE); } void fe_set_auto_dark_mode_state (gboolean enabled) { auto_dark_mode_enabled = enabled; dark_mode_state_initialized = TRUE; } gboolean fe_dark_mode_state_is_initialized (void) { return dark_mode_state_initialized; } void fe_refresh_auto_dark_mode (void) { fe_auto_dark_mode_changed (NULL, NULL, NULL); } gboolean fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed) { gboolean enabled = fe_dark_mode_is_enabled_for (mode); GList *toplevels, *node; /* Apply the optional global GTK preference first, then reapply palette-driven * chat/input colors so Preferences > Colors continues to take precedence. */ fe_set_gtk_prefer_dark_theme (enabled); gboolean changed = palette_apply_dark_mode (enabled); #ifdef G_OS_WIN32 fe_apply_windows_theme (enabled); #endif if (palette_changed) *palette_changed = changed; if (input_style) create_input_style (input_style); if (!fe_apply_gtk3_theme (prefs.hex_gui_gtk3_theme_name, NULL) && prefs.hex_gui_gtk3_theme_name[0] != '\0') { fe_message (_("Failed to apply configured GTK3 theme from gtk3-themes."), FE_MSG_ERROR); } /* Existing toplevel windows also need the class refreshed for selectors like * .zoitechat-dark / .zoitechat-light to update immediately. */ toplevels = gtk_window_list_toplevels (); for (node = toplevels; node; node = node->next) fe_apply_theme_to_toplevel (GTK_WIDGET (node->data)); g_list_free (toplevels); return enabled; } void fe_apply_theme_to_toplevel (GtkWidget *window) { GtkStyleContext *context; gboolean dark; if (!window) return; context = gtk_widget_get_style_context (window); dark = fe_dark_mode_is_enabled (); if (context) { gtk_style_context_remove_class (context, "zoitechat-dark"); gtk_style_context_remove_class (context, "zoitechat-light"); gtk_style_context_add_class (context, dark ? "zoitechat-dark" : "zoitechat-light"); } fe_win32_apply_native_titlebar (window, dark); } gboolean fe_dark_mode_is_enabled_for (unsigned int mode) { switch (mode) { case ZOITECHAT_DARK_MODE_DARK: return TRUE; case ZOITECHAT_DARK_MODE_LIGHT: return FALSE; case ZOITECHAT_DARK_MODE_AUTO: default: return auto_dark_mode_enabled; } } gboolean fe_dark_mode_is_enabled (void) { return fe_dark_mode_is_enabled_for (prefs.hex_gui_dark_mode); } InputStyle * create_input_style (InputStyle *style) { char buf[256]; static int done_rc = FALSE; static GtkCssProvider *input_css_provider = NULL; static char *last_theme_name = NULL; static gboolean last_dark_mode = FALSE; static gboolean last_input_style = FALSE; static gboolean last_colors_set = FALSE; static guint16 last_fg_red; static guint16 last_fg_green; static guint16 last_fg_blue; static guint16 last_bg_red; static guint16 last_bg_green; static guint16 last_bg_blue; static guint16 last_sel_fg_red; static guint16 last_sel_fg_green; static guint16 last_sel_fg_blue; static guint16 last_sel_bg_red; static guint16 last_sel_bg_green; static guint16 last_sel_bg_blue; if (!style) style = g_new0 (InputStyle, 1); if (style->font_desc) pango_font_description_free (style->font_desc); style->font_desc = pango_font_description_from_string (prefs.hex_text_font); /* fall back */ if (pango_font_description_get_size (style->font_desc) == 0) { g_snprintf (buf, sizeof (buf), _("Failed to open font:\n\n%s"), prefs.hex_text_font); fe_message (buf, FE_MSG_ERROR); pango_font_description_free (style->font_desc); style->font_desc = pango_font_description_from_string ("sans 11"); } if (prefs.hex_gui_input_style) { GtkSettings *settings = gtk_settings_get_default (); GdkScreen *screen = gdk_screen_get_default (); char *theme_name; char cursor_rc[sizeof (cursor_color_rc)]; char cursor_color[8]; const char *cursor_color_start = NULL; guint16 fg_red; guint16 fg_green; guint16 fg_blue; guint16 bg_red; guint16 bg_green; guint16 bg_blue; guint16 sel_fg_red; guint16 sel_fg_green; guint16 sel_fg_blue; guint16 sel_bg_red; guint16 sel_bg_green; guint16 sel_bg_blue; gboolean dark_mode = fe_dark_mode_is_enabled (); gboolean needs_reload; g_object_get (settings, "gtk-theme-name", &theme_name, NULL); palette_color_get_rgb16 (&colors[COL_FG], &fg_red, &fg_green, &fg_blue); palette_color_get_rgb16 (&colors[COL_BG], &bg_red, &bg_green, &bg_blue); palette_color_get_rgb16 (&colors[COL_MARK_FG], &sel_fg_red, &sel_fg_green, &sel_fg_blue); palette_color_get_rgb16 (&colors[COL_MARK_BG], &sel_bg_red, &sel_bg_green, &sel_bg_blue); needs_reload = !done_rc || !last_input_style || last_dark_mode != dark_mode || g_strcmp0 (last_theme_name, theme_name) != 0 || !last_colors_set || last_fg_red != fg_red || last_fg_green != fg_green || last_fg_blue != fg_blue || last_bg_red != bg_red || last_bg_green != bg_green || last_bg_blue != bg_blue || last_sel_fg_red != sel_fg_red || last_sel_fg_green != sel_fg_green || last_sel_fg_blue != sel_fg_blue || last_sel_bg_red != sel_bg_red || last_sel_bg_green != sel_bg_green || last_sel_bg_blue != sel_bg_blue; if (needs_reload) { if (!input_css_provider) input_css_provider = gtk_css_provider_new (); g_snprintf (buf, sizeof (buf), "#%02x%02x%02x", (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8)); g_snprintf (cursor_rc, sizeof (cursor_rc), cursor_color_rc, (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8)); cursor_color_start = g_strstr_len (cursor_rc, -1, "cursor-color=\""); if (cursor_color_start) { cursor_color_start += strlen ("cursor-color=\""); g_strlcpy (cursor_color, cursor_color_start, sizeof (cursor_color)); cursor_color[7] = '\0'; } else { g_strlcpy (cursor_color, buf, sizeof (cursor_color)); } { GString *css = g_string_new ("#zoitechat-inputbox {"); GtkWidget *tmp_entry = NULL; GtkStyleContext *tmp_context = NULL; GdkRGBA selected_fg = { 0.0, 0.0, 0.0, 1.0 }; GdkRGBA selected_bg = { 0.0, 0.0, 0.0, 1.0 }; gboolean have_palette_selected_colors; const char *selection_fg_css = NULL; const char *selection_bg_css = NULL; char selection_fg_hex[8]; char selection_bg_hex[8]; char *selection_fg_fallback = NULL; char *selection_bg_fallback = NULL; /* GTK3 equivalents for adwaita_workaround_rc/cursor_color_rc. */ if (adwaita_workaround_rc[0] != '\0' && theme_name && (g_str_has_prefix (theme_name, "Adwaita") || g_str_has_prefix (theme_name, "Yaru"))) g_string_append (css, "background-image: none;"); have_palette_selected_colors = isfinite (colors[COL_MARK_FG].red) && isfinite (colors[COL_MARK_FG].green) && isfinite (colors[COL_MARK_FG].blue) && isfinite (colors[COL_MARK_BG].red) && isfinite (colors[COL_MARK_BG].green) && isfinite (colors[COL_MARK_BG].blue); if (have_palette_selected_colors) { g_snprintf (selection_fg_hex, sizeof (selection_fg_hex), "#%02x%02x%02x", (sel_fg_red >> 8), (sel_fg_green >> 8), (sel_fg_blue >> 8)); g_snprintf (selection_bg_hex, sizeof (selection_bg_hex), "#%02x%02x%02x", (sel_bg_red >> 8), (sel_bg_green >> 8), (sel_bg_blue >> 8)); selection_fg_css = selection_fg_hex; selection_bg_css = selection_bg_hex; } else { tmp_entry = gtk_entry_new (); tmp_context = tmp_entry ? gtk_widget_get_style_context (tmp_entry) : NULL; if (tmp_context) { if (!gtk_style_context_lookup_color ( tmp_context, "theme_selected_fg_color", &selected_fg)) selected_fg = colors[COL_MARK_FG]; if (!gtk_style_context_lookup_color ( tmp_context, "theme_selected_bg_color", &selected_bg)) selected_bg = colors[COL_MARK_BG]; } else { selected_fg = colors[COL_MARK_FG]; selected_bg = colors[COL_MARK_BG]; } selection_fg_fallback = gdk_rgba_to_string (&selected_fg); selection_bg_fallback = gdk_rgba_to_string (&selected_bg); selection_fg_css = selection_fg_fallback ? selection_fg_fallback : "@theme_selected_fg_color"; selection_bg_css = selection_bg_fallback ? selection_bg_fallback : "@theme_selected_bg_color"; } g_string_append_printf ( css, "background-color: #%02x%02x%02x;" "caret-color: %s;" "}" "#zoitechat-inputbox text {" "color: #%02x%02x%02x;" "caret-color: %s;" "}" "#zoitechat-inputbox:focus text," "#zoitechat-inputbox:backdrop text {" "color: #%02x%02x%02x;" "caret-color: %s;" "}" "#zoitechat-inputbox:disabled text {" "color: alpha(#%02x%02x%02x, 0.7);" "}" "#zoitechat-inputbox text selection {" "color: %s;" "background-color: %s;" "}", (bg_red >> 8), (bg_green >> 8), (bg_blue >> 8), cursor_color, (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), cursor_color, (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), cursor_color, (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), selection_fg_css, selection_bg_css); if (tmp_entry) gtk_widget_destroy (tmp_entry); g_clear_pointer (&selection_fg_fallback, g_free); g_clear_pointer (&selection_bg_fallback, g_free); gtk_css_provider_load_from_data (input_css_provider, css->str, -1, NULL); g_string_free (css, TRUE); } if (screen) gtk_style_context_add_provider_for_screen ( screen, GTK_STYLE_PROVIDER (input_css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); done_rc = TRUE; last_input_style = TRUE; last_dark_mode = dark_mode; last_colors_set = TRUE; last_fg_red = fg_red; last_fg_green = fg_green; last_fg_blue = fg_blue; last_bg_red = bg_red; last_bg_green = bg_green; last_bg_blue = bg_blue; last_sel_fg_red = sel_fg_red; last_sel_fg_green = sel_fg_green; last_sel_fg_blue = sel_fg_blue; last_sel_bg_red = sel_bg_red; last_sel_bg_green = sel_bg_green; last_sel_bg_blue = sel_bg_blue; g_free (last_theme_name); last_theme_name = g_strdup (theme_name); } g_free (theme_name); } else { GdkScreen *screen = gdk_screen_get_default (); if (input_css_provider && screen) { gtk_style_context_remove_provider_for_screen ( screen, GTK_STYLE_PROVIDER (input_css_provider)); } g_clear_object (&input_css_provider); g_clear_pointer (&last_theme_name, g_free); done_rc = FALSE; last_input_style = FALSE; last_colors_set = FALSE; } return style; } void fe_init (void) { GtkSettings *settings; palette_load (); settings = gtk_settings_get_default (); if (settings) { auto_dark_mode_enabled = fe_system_prefers_dark (); dark_mode_state_initialized = TRUE; } fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL); key_init (); pixmaps_init (); #ifdef HAVE_GTK_MAC gtkosx_application_set_dock_icon_pixbuf (osx_app, pix_zoitechat); #endif channelwin_pix = pixmap_load_from_file (prefs.hex_text_background); input_style = create_input_style (input_style); if (settings) { g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK (fe_auto_dark_mode_changed), NULL); g_signal_connect (settings, "notify::gtk-theme-name", G_CALLBACK (fe_auto_dark_mode_changed), NULL); } } #ifdef HAVE_GTK_MAC static void gtkosx_application_terminate (GtkosxApplication *app, gpointer userdata) { zoitechat_exit(); } #endif void fe_main (void) { #ifdef HAVE_GTK_MAC gtkosx_application_ready(osx_app); g_signal_connect (G_OBJECT(osx_app), "NSApplicationWillTerminate", G_CALLBACK(gtkosx_application_terminate), NULL); #endif gtk_main (); /* sleep for 2 seconds so any QUIT messages are not lost. The */ /* GUI is closed at this point, so the user doesn't even know! */ if (prefs.wait_on_exit) sleep (2); } void fe_cleanup (void) { } void fe_exit (void) { gtk_main_quit (); } int fe_timeout_add (int interval, void *callback, void *userdata) { return g_timeout_add (interval, (GSourceFunc) callback, userdata); } int fe_timeout_add_seconds (int interval, void *callback, void *userdata) { return g_timeout_add_seconds (interval, (GSourceFunc) callback, userdata); } void fe_timeout_remove (int tag) { g_source_remove (tag); } #ifdef WIN32 static void log_handler (const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer unused_data) { session *sess; return; sess = find_dialog (serv_list->data, "(warnings)"); if (!sess) sess = new_ircwindow (serv_list->data, "(warnings)", SESS_DIALOG, 0); PrintTextf (sess, "%s\t%s\n", log_domain, message); if (getenv ("ZOITECHAT_WARNING_ABORT")) abort (); } #endif static int fe_idle (gpointer data) { session *sess = sess_list->data; plugin_add (sess, NULL, NULL, notification_plugin_init, notification_plugin_deinit, NULL, FALSE); plugin_add (sess, NULL, NULL, tray_plugin_init, tray_plugin_deinit, NULL, FALSE); if (arg_minimize == 1) gtk_window_iconify (GTK_WINDOW (sess->gui->window)); else if (arg_minimize == 2) tray_toggle_visibility (FALSE); return 0; } void fe_new_window (session *sess, int focus) { int tab = FALSE; if (sess->type == SESS_DIALOG) { if (prefs.hex_gui_tab_dialogs) tab = TRUE; } else { if (prefs.hex_gui_tab_chans) tab = TRUE; } mg_changui_new (sess, NULL, tab, focus); #ifdef WIN32 g_log_set_handler ("GLib", G_LOG_LEVEL_CRITICAL|G_LOG_LEVEL_WARNING, (GLogFunc)log_handler, 0); g_log_set_handler ("GLib-GObject", G_LOG_LEVEL_CRITICAL|G_LOG_LEVEL_WARNING, (GLogFunc)log_handler, 0); g_log_set_handler ("Gdk", G_LOG_LEVEL_CRITICAL|G_LOG_LEVEL_WARNING, (GLogFunc)log_handler, 0); g_log_set_handler ("Gtk", G_LOG_LEVEL_CRITICAL|G_LOG_LEVEL_WARNING, (GLogFunc)log_handler, 0); #endif if (!sess_list->next) g_idle_add (fe_idle, NULL); sess->scrollback_replay_marklast = gtk_xtext_set_marker_last; } void fe_new_server (struct server *serv) { serv->gui = g_new0 (struct server_gui, 1); } void fe_message (char *msg, int flags) { GtkWidget *dialog; int type = GTK_MESSAGE_WARNING; if (flags & FE_MSG_ERROR) type = GTK_MESSAGE_ERROR; if (flags & FE_MSG_INFO) type = GTK_MESSAGE_INFO; dialog = gtk_message_dialog_new (GTK_WINDOW (parent_window), 0, type, GTK_BUTTONS_OK, "%s", msg); /* Window classes are required for GTK CSS selectors like * .zoitechat-dark / .zoitechat-light. */ fe_apply_theme_to_toplevel (dialog); if (flags & FE_MSG_MARKUP) gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), msg); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (gtk_widget_destroy), 0); gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE); gtk_widget_show (dialog); if (flags & FE_MSG_WAIT) gtk_dialog_run (GTK_DIALOG (dialog)); } void fe_idle_add (void *func, void *data) { g_idle_add (func, data); } void fe_input_remove (int tag) { g_source_remove (tag); } int fe_input_add (int sok, int flags, void *func, void *data) { int tag, type = 0; GIOChannel *channel; #ifdef WIN32 if (flags & FIA_FD) channel = g_io_channel_win32_new_fd (sok); else channel = g_io_channel_win32_new_socket (sok); #else channel = g_io_channel_unix_new (sok); #endif if (flags & FIA_READ) type |= G_IO_IN | G_IO_HUP | G_IO_ERR; if (flags & FIA_WRITE) type |= G_IO_OUT | G_IO_ERR; if (flags & FIA_EX) type |= G_IO_PRI; tag = g_io_add_watch (channel, type, (GIOFunc) func, data); g_io_channel_unref (channel); return tag; } void fe_set_topic (session *sess, char *topic, char *stripped_topic) { if (!sess->gui->is_tab || sess == current_tab) { if (prefs.hex_text_stripcolor_topic) { gtk_entry_set_text (GTK_ENTRY (sess->gui->topic_entry), stripped_topic); } else { gtk_entry_set_text (GTK_ENTRY (sess->gui->topic_entry), topic); } mg_set_topic_tip (sess); } else { g_free (sess->res->topic_text); if (prefs.hex_text_stripcolor_topic) { sess->res->topic_text = g_strdup (stripped_topic); } else { sess->res->topic_text = g_strdup (topic); } } } static void fe_update_mode_entry (session *sess, GtkWidget *entry, char **text, char *new_text) { if (!sess->gui->is_tab || sess == current_tab) { if (sess->gui->flag_wid[0]) /* channel mode buttons enabled? */ gtk_entry_set_text (GTK_ENTRY (entry), new_text); } else { if (sess->gui->is_tab) { g_free (*text); *text = g_strdup (new_text); } } } void fe_update_channel_key (struct session *sess) { fe_update_mode_entry (sess, sess->gui->key_entry, &sess->res->key_text, sess->channelkey); fe_set_title (sess); } void fe_update_channel_limit (struct session *sess) { char tmp[16]; sprintf (tmp, "%d", sess->limit); fe_update_mode_entry (sess, sess->gui->limit_entry, &sess->res->limit_text, tmp); fe_set_title (sess); } int fe_is_chanwindow (struct server *serv) { if (!serv->gui->chanlist_window) return 0; return 1; } void fe_notify_update (char *name) { if (!name) notify_gui_update (); } void fe_text_clear (struct session *sess, int lines) { gtk_xtext_clear (sess->res->buffer, lines); } void fe_close_window (struct session *sess) { if (sess->gui->is_tab) mg_tab_close (sess); else gtk_widget_destroy (sess->gui->window); } void fe_progressbar_start (session *sess) { if (!sess->gui->is_tab || current_tab == sess) /* if it's the focused tab, create it for real! */ mg_progressbar_create (sess->gui); else /* otherwise just remember to create on when it gets focused */ sess->res->c_graph = TRUE; } void fe_progressbar_end (server *serv) { GSList *list = sess_list; session *sess; while (list) /* check all windows that use this server and * * remove the connecting graph, if it has one. */ { sess = list->data; if (sess->server == serv) { if (sess->gui->bar) mg_progressbar_destroy (sess->gui); sess->res->c_graph = FALSE; } list = list->next; } } void fe_print_text (struct session *sess, char *text, time_t stamp, gboolean no_activity) { PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp); if (no_activity || !sess->gui->is_tab) return; if (sess == current_tab) fe_set_tab_color (sess, FE_COLOR_NONE); else if (sess->tab_state & TAB_STATE_NEW_HILIGHT) fe_set_tab_color (sess, FE_COLOR_NEW_HILIGHT); else if (sess->tab_state & TAB_STATE_NEW_MSG) fe_set_tab_color (sess, FE_COLOR_NEW_MSG); else fe_set_tab_color (sess, FE_COLOR_NEW_DATA); } void fe_beep (session *sess) { #ifdef WIN32 /* Play the "Instant Message Notification" system sound */ if (!PlaySoundW (L"Notification.IM", NULL, SND_ALIAS | SND_ASYNC)) { /* The user does not have the "Instant Message Notification" sound set. Fall back to system beep. */ Beep (1000, 50); } #else #ifdef USE_LIBCANBERRA if (ca_con == NULL) { ca_context_create (&ca_con); ca_context_change_props (ca_con, CA_PROP_APPLICATION_ID, "zoitechat", CA_PROP_APPLICATION_NAME, DISPLAY_NAME, CA_PROP_APPLICATION_ICON_NAME, "zoitechat", NULL); } if (ca_context_play (ca_con, 0, CA_PROP_EVENT_ID, "message-new-instant", NULL) != 0) #endif gdk_beep (); #endif } void fe_lastlog (session *sess, session *lastlog_sess, char *sstr, gtk_xtext_search_flags flags) { GError *err = NULL; xtext_buffer *buf, *lbuf; buf = sess->res->buffer; if (gtk_xtext_is_empty (buf)) { PrintText (lastlog_sess, _("Search buffer is empty.\n")); return; } lbuf = lastlog_sess->res->buffer; if (flags & regexp) { GRegexCompileFlags gcf = (flags & case_match)? 0: G_REGEX_CASELESS; lbuf->search_re = g_regex_new (sstr, gcf, 0, &err); if (err) { PrintText (lastlog_sess, _(err->message)); g_error_free (err); return; } } else { if (flags & case_match) { lbuf->search_nee = g_strdup (sstr); } else { lbuf->search_nee = g_utf8_casefold (sstr, strlen (sstr)); } lbuf->search_lnee = strlen (lbuf->search_nee); } lbuf->search_flags = flags; lbuf->search_text = g_strdup (sstr); gtk_xtext_lastlog (lbuf, buf); } void fe_set_lag (server *serv, long lag) { GSList *list = sess_list; session *sess; gdouble per; char lagtext[64]; char lagtip[128]; unsigned long nowtim; if (lag == -1) { if (!serv->lag_sent) return; nowtim = make_ping_time (); lag = nowtim - serv->lag_sent; } /* if there is no pong for >30s report the lag as +30s */ if (lag > 30000 && serv->lag_sent) lag=30000; per = ((double)lag) / 1000.0; if (per > 1.0) per = 1.0; g_snprintf (lagtext, sizeof (lagtext) - 1, "%s%ld.%lds", serv->lag_sent ? "+" : "", lag / 1000, (lag/100) % 10); g_snprintf (lagtip, sizeof (lagtip) - 1, "Lag: %s%ld.%ld seconds", serv->lag_sent ? "+" : "", lag / 1000, (lag/100) % 10); while (list) { sess = list->data; if (sess->server == serv) { g_free (sess->res->lag_tip); sess->res->lag_tip = g_strdup (lagtip); if (!sess->gui->is_tab || current_tab == sess) { if (sess->gui->lagometer) { gtk_progress_bar_set_fraction ((GtkProgressBar *) sess->gui->lagometer, per); gtk_widget_set_tooltip_text (gtk_widget_get_parent (sess->gui->lagometer), lagtip); } if (sess->gui->laginfo) gtk_label_set_text ((GtkLabel *) sess->gui->laginfo, lagtext); } else { sess->res->lag_value = per; g_free (sess->res->lag_text); sess->res->lag_text = g_strdup (lagtext); } } list = list->next; } } void fe_set_throttle (server *serv) { GSList *list = sess_list; struct session *sess; float per; char tbuf[96]; char tip[160]; per = (float) serv->sendq_len / 1024.0; if (per > 1.0) per = 1.0; while (list) { sess = list->data; if (sess->server == serv) { g_snprintf (tbuf, sizeof (tbuf) - 1, _("%d bytes"), serv->sendq_len); g_snprintf (tip, sizeof (tip) - 1, _("Network send queue: %d bytes"), serv->sendq_len); g_free (sess->res->queue_tip); sess->res->queue_tip = g_strdup (tip); if (!sess->gui->is_tab || current_tab == sess) { if (sess->gui->throttlemeter) { gtk_progress_bar_set_fraction ((GtkProgressBar *) sess->gui->throttlemeter, per); gtk_widget_set_tooltip_text (gtk_widget_get_parent (sess->gui->throttlemeter), tip); } if (sess->gui->throttleinfo) gtk_label_set_text ((GtkLabel *) sess->gui->throttleinfo, tbuf); } else { sess->res->queue_value = per; g_free (sess->res->queue_text); sess->res->queue_text = g_strdup (tbuf); } } list = list->next; } } void fe_ctrl_gui (session *sess, fe_gui_action action, int arg) { switch (action) { case FE_GUI_HIDE: gtk_widget_hide (sess->gui->window); break; case FE_GUI_SHOW: gtk_widget_show (sess->gui->window); gtk_window_present (GTK_WINDOW (sess->gui->window)); break; case FE_GUI_FOCUS: mg_bring_tofront_sess (sess); break; case FE_GUI_FLASH: fe_flash_window (sess); break; case FE_GUI_COLOR: fe_set_tab_color (sess, arg); break; case FE_GUI_ICONIFY: gtk_window_iconify (GTK_WINDOW (sess->gui->window)); break; case FE_GUI_MENU: menu_bar_toggle (); /* toggle menubar on/off */ break; case FE_GUI_ATTACH: mg_detach (sess, arg); /* arg: 0=toggle 1=detach 2=attach */ break; case FE_GUI_APPLY: /* Keep parity with Preferences -> Theme apply path (setup_theme_apply_cb). */ palette_load (); fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL); setup_apply_real (TRUE, TRUE, TRUE, FALSE); } } static void dcc_saveas_cb (struct DCC *dcc, char *file) { if (is_dcc (dcc)) { if (dcc->dccstat == STAT_QUEUED) { if (file) dcc_get_with_destfile (dcc, file); else if (dcc->resume_sent == 0) dcc_abort (dcc->serv->front_session, dcc); } } } void fe_confirm (const char *message, void (*yesproc)(void *), void (*noproc)(void *), void *ud) { /* warning, assuming fe_confirm is used by DCC only! */ struct DCC *dcc = ud; if (dcc->file) { char *filepath = g_build_filename (prefs.hex_dcc_dir, dcc->file, NULL); gtkutil_file_req (NULL, message, dcc_saveas_cb, ud, filepath, NULL, FRF_WRITE|FRF_NOASKOVERWRITE|FRF_FILTERISINITIAL); g_free (filepath); } } int fe_gui_info (session *sess, int info_type) { switch (info_type) { case 0: /* window status */ if (!gtk_widget_get_visible (GTK_WIDGET (sess->gui->window))) { return 2; /* hidden (iconified or systray) */ } if (gtk_window_is_active (GTK_WINDOW (sess->gui->window))) { return 1; /* active/focused */ } return 0; /* normal (no keyboard focus or behind a window) */ } return -1; } void * fe_gui_info_ptr (session *sess, int info_type) { switch (info_type) { case 0: /* native window pointer (for plugins) */ #ifdef GDK_WINDOWING_WIN32 return gdk_win32_window_get_handle (gtk_widget_get_window (sess->gui->window)); #else return sess->gui->window; #endif break; case 1: /* GtkWindow * (for plugins) */ return sess->gui->window; } return NULL; } char * fe_get_inputbox_contents (session *sess) { /* not the current tab */ if (sess->res->input_text) return sess->res->input_text; /* current focused tab */ return SPELL_ENTRY_GET_TEXT (sess->gui->input_box); } int fe_get_inputbox_cursor (session *sess) { /* not the current tab (we don't remember the cursor pos) */ if (sess->res->input_text) return 0; /* current focused tab */ return SPELL_ENTRY_GET_POS (sess->gui->input_box); } void fe_set_inputbox_cursor (session *sess, int delta, int pos) { if (!sess->gui->is_tab || sess == current_tab) { if (delta) pos += SPELL_ENTRY_GET_POS (sess->gui->input_box); SPELL_ENTRY_SET_POS (sess->gui->input_box, pos); } else { /* we don't support changing non-front tabs yet */ } } void fe_set_inputbox_contents (session *sess, char *text) { if (!sess->gui->is_tab || sess == current_tab) { SPELL_ENTRY_SET_TEXT (sess->gui->input_box, text); } else { g_free (sess->res->input_text); sess->res->input_text = g_strdup (text); } } #ifdef __APPLE__ static char * url_escape_hostname (const char *url) { char *host_start, *host_end, *ret, *hostname; host_start = strstr (url, "://"); if (host_start != NULL) { *host_start = '\0'; host_start += 3; host_end = strchr (host_start, '/'); if (host_end != NULL) { *host_end = '\0'; host_end++; } hostname = g_hostname_to_ascii (host_start); if (host_end != NULL) ret = g_strdup_printf ("%s://%s/%s", url, hostname, host_end); else ret = g_strdup_printf ("%s://%s", url, hostname); g_free (hostname); return ret; } return g_strdup (url); } static void osx_show_uri (const char *url) { char *escaped_url, *encoded_url, *open, *cmd; escaped_url = url_escape_hostname (url); encoded_url = g_filename_from_utf8 (escaped_url, -1, NULL, NULL, NULL); if (encoded_url) { open = g_find_program_in_path ("open"); cmd = g_strjoin (" ", open, encoded_url, NULL); zoitechat_exec (cmd); g_free (encoded_url); g_free (cmd); } g_free (escaped_url); } #endif static inline char * escape_uri (const char *uri) { return g_uri_escape_string(uri, G_URI_RESERVED_CHARS_GENERIC_DELIMITERS G_URI_RESERVED_CHARS_SUBCOMPONENT_DELIMITERS, FALSE); } static inline gboolean uri_contains_forbidden_characters (const char *uri) { while (*uri) { if (!g_ascii_isalnum (*uri) && !strchr ("-._~:/?#[]@!$&'()*+,;=", *uri)) return TRUE; uri++; } return FALSE; } static char * maybe_escape_uri (const char *uri) { /* The only way to know if a string has already been escaped or not * is by fulling parsing each segement but we can try some more simple heuristics. */ /* If we find characters that should clearly be escaped. */ if (uri_contains_forbidden_characters (uri)) return escape_uri (uri); /* If it fails to be unescaped then it was not escaped. */ char *unescaped = g_uri_unescape_string (uri, NULL); if (!unescaped) return escape_uri (uri); g_free (unescaped); /* At this point it is probably safe to pass through as-is. */ return g_strdup (uri); } static void fe_open_url_inner (const char *url) { #ifdef WIN32 gunichar2 *url_utf16 = g_utf8_to_utf16 (url, -1, NULL, NULL, NULL); if (url_utf16 == NULL) { return; } ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL); g_free (url_utf16); #elif defined(__APPLE__) osx_show_uri (url); #else GError *error = NULL; char *escaped_url = maybe_escape_uri (url); gchar *xdg_open_argv[] = {(gchar *) "xdg-open", escaped_url, NULL}; gchar **spawn_env = NULL; gboolean opened = FALSE; g_debug ("Opening URL \"%s\" (%s)", escaped_url, url); /* AppImage runtime variables can point host binaries like /bin/sh at * bundled libraries, which may not be ABI-compatible with system tools. */ spawn_env = g_get_environ (); { gchar **tmp_env = spawn_env; spawn_env = g_environ_unsetenv (tmp_env, "LD_LIBRARY_PATH"); if (spawn_env != tmp_env) g_strfreev (tmp_env); tmp_env = spawn_env; spawn_env = g_environ_unsetenv (tmp_env, "LD_PRELOAD"); if (spawn_env != tmp_env) g_strfreev (tmp_env); } /* Prefer xdg-open when available because gtk_show_uri can inherit * AppImage runtime state and fail before we can control the environment. */ { gchar *xdg_open_path = g_find_program_in_path ("xdg-open"); if (xdg_open_path && g_spawn_async (NULL, xdg_open_argv, spawn_env, G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, &error)) { opened = TRUE; } else { g_clear_error (&error); } g_free (xdg_open_path); } if (!opened && gtk_show_uri (NULL, escaped_url, GDK_CURRENT_TIME, &error)) { opened = TRUE; } else if (!opened) { g_warning ("gtk_show_uri failed for '%s': %s", escaped_url, error ? error->message : "unknown error"); g_clear_error (&error); } if (!opened) { g_warning ("Unable to open URL '%s' via xdg-open or gtk_show_uri", escaped_url); } g_strfreev (spawn_env); g_free (escaped_url); #endif } void fe_open_url (const char *url) { int url_type = url_check_word (url); char *uri; /* gvfs likes file:// */ if (url_type == WORD_PATH) { #ifndef WIN32 uri = g_strconcat ("file://", url, NULL); fe_open_url_inner (uri); g_free (uri); #else fe_open_url_inner (url); #endif } /* IPv6 addr. Add http:// */ else if (url_type == WORD_HOST6) { /* IPv6 addrs in urls should be enclosed in [ ] */ if (*url != '[') uri = g_strdup_printf ("http://[%s]", url); else uri = g_strdup_printf ("http://%s", url); fe_open_url_inner (uri); g_free (uri); } /* the http:// part's missing, prepend it, otherwise it won't always work */ else if (strchr (url, ':') == NULL) { uri = g_strdup_printf ("http://%s", url); fe_open_url_inner (uri); g_free (uri); } /* we have a sane URL, send it to the browser untouched */ else { fe_open_url_inner (url); } } void fe_server_event (server *serv, int type, int arg) { GSList *list = sess_list; session *sess; while (list) { sess = list->data; if (sess->server == serv && (current_tab == sess || !sess->gui->is_tab)) { session_gui *gui = sess->gui; switch (type) { case FE_SE_CONNECTING: /* connecting in progress */ case FE_SE_RECONDELAY: /* reconnect delay begun */ /* enable Disconnect item */ gtk_widget_set_sensitive (gui->menu_item[MENU_ID_DISCONNECT], 1); break; case FE_SE_CONNECT: /* enable Disconnect and Away menu items */ gtk_widget_set_sensitive (gui->menu_item[MENU_ID_AWAY], 1); gtk_widget_set_sensitive (gui->menu_item[MENU_ID_DISCONNECT], 1); break; case FE_SE_LOGGEDIN: /* end of MOTD */ gtk_widget_set_sensitive (gui->menu_item[MENU_ID_JOIN], 1); /* if number of auto-join channels is zero, open joind */ if (arg == 0) joind_open (serv); break; case FE_SE_DISCONNECT: /* disable Disconnect and Away menu items */ gtk_widget_set_sensitive (gui->menu_item[MENU_ID_AWAY], 0); gtk_widget_set_sensitive (gui->menu_item[MENU_ID_DISCONNECT], 0); gtk_widget_set_sensitive (gui->menu_item[MENU_ID_JOIN], 0); /* close the join-dialog, if one exists */ joind_close (serv); } } list = list->next; } } void fe_get_file (const char *title, char *initial, void (*callback) (void *userdata, char *file), void *userdata, int flags) { /* OK: Call callback once per file, then once more with file=NULL. */ /* CANCEL: Call callback once with file=NULL. */ gtkutil_file_req (NULL, title, callback, userdata, initial, NULL, flags | FRF_FILTERISINITIAL); } void fe_open_chan_list (server *serv, char *filter, int do_refresh) { chanlist_opengui (serv, do_refresh); } const char * fe_get_default_font (void) { #ifdef WIN32 if (gtkutil_find_font ("Consolas")) return "Consolas 10"; else #else #ifdef __APPLE__ if (gtkutil_find_font ("Menlo")) return "Menlo 13"; else #endif #endif return NULL; }