Files
zoitechat/src/fe-gtk/fe-gtk.c
deepend 90ada474a0 Added GTK3 theme resource lifecycle management so ZoiteChat now unregisters prior theme resources and can register a theme’s gtk.gresource bundle when available (gtk-3.0/gtk.gresource). This improves compatibility with full GTK3 themes that depend on bundled resource URIs, not just plain CSS files.
Extended fe_apply_gtk3_theme_with_reload to:

detect gtk.gresource,

register it before applying CSS,

unregister resources when no resource file is present or when switching back to system theme,

and clean up allocated paths consistently on error paths
2026-02-26 08:04:27 -07:00

2221 lines
54 KiB
C

/* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include "fe-gtk.h"
#ifdef GDK_WINDOWING_WIN32
#include <gdk/gdkwin32.h>
#endif
#ifdef WIN32
#include <windows.h>
#include <dwmapi.h>
#else
#include <unistd.h>
#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 <canberra.h>
#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 (&gtk3_theme_resource, g_resource_unref);
}
g_clear_pointer (&gtk3_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 (&gtk3_theme_provider);
g_clear_pointer (&gtk3_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;
}