diff --git a/plugins/python/python.py b/plugins/python/python.py index a2cdb094..bab732f7 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -19,7 +19,7 @@ else: if not hasattr(sys, 'argv'): sys.argv = [''] -VERSION = b'2.0' # Sync with zoitechat.__version__ +VERSION = b'2.18.0-pre1' # Sync with zoitechat.__version__ PLUGIN_NAME = ffi.new('char[]', b'Python') PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1])) PLUGIN_VERSION = ffi.new('char[]', VERSION) @@ -591,4 +591,4 @@ def _on_plugin_deinit(): except KeyError: pass - return 1 \ No newline at end of file + return 1 diff --git a/src/common/outbound.c b/src/common/outbound.c index aae7c830..3a4d6a10 100644 --- a/src/common/outbound.c +++ b/src/common/outbound.c @@ -3783,9 +3783,19 @@ cmd_url (struct session *sess, char *tbuf, char *word[], char *word_eol[]) if (zoitechat_import_theme (theme_path, &error)) { - message = g_strdup_printf (_("Theme \"%s\" imported."), basename); - fe_message (message, FE_MSG_INFO); - g_free (message); + if (zoitechat_apply_theme (basename, &error)) + { + message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); + fe_message (message, FE_MSG_INFO); + handle_command (sess, "gui apply", FALSE); + g_free (message); + } + else + { + fe_message (error ? error->message : _("Theme imported, but failed to apply."), + FE_MSG_ERROR); + g_clear_error (&error); + } } else { diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index d1b962b8..dbb6e749 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -247,6 +247,85 @@ zoitechat_remote_win32 (void) } #endif + +static gboolean +zoitechat_copy_theme_file (const char *src, const char *dest, GError **error) +{ + char *data = NULL; + gsize len = 0; + + if (!g_file_get_contents (src, &data, &len, error)) + return FALSE; + + if (!g_file_set_contents (dest, data, len, error)) + { + g_free (data); + return FALSE; + } + + g_free (data); + return TRUE; +} + +gboolean +zoitechat_apply_theme (const char *theme_name, GError **error) +{ + char *theme_dir; + char *colors_src; + char *colors_dest; + char *events_src; + char *events_dest; + gboolean ok = FALSE; + + if (!theme_name || !*theme_name) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("No theme name specified.")); + return FALSE; + } + + theme_dir = g_build_filename (get_xdir (), "themes", theme_name, NULL); + colors_src = g_build_filename (theme_dir, "colors.conf", NULL); + colors_dest = g_build_filename (get_xdir (), "colors.conf", NULL); + events_src = g_build_filename (theme_dir, "pevents.conf", NULL); + events_dest = g_build_filename (get_xdir (), "pevents.conf", NULL); + + if (!g_file_test (colors_src, G_FILE_TEST_IS_REGULAR)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("This theme is missing a colors.conf file.")); + goto cleanup; + } + + if (!zoitechat_copy_theme_file (colors_src, colors_dest, error)) + goto cleanup; + + if (g_file_test (events_src, G_FILE_TEST_IS_REGULAR)) + { + if (!zoitechat_copy_theme_file (events_src, events_dest, error)) + goto cleanup; + } + else if (g_file_test (events_dest, G_FILE_TEST_EXISTS)) + { + if (g_unlink (events_dest) != 0) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Failed to remove existing event settings.")); + goto cleanup; + } + } + + ok = TRUE; + +cleanup: + g_free (events_dest); + g_free (events_src); + g_free (colors_dest); + g_free (colors_src); + g_free (theme_dir); + return ok; +} + gboolean zoitechat_import_theme (const char *path, GError **error) { @@ -754,9 +833,19 @@ irc_init (session *sess) if (zoitechat_import_theme (theme_path, &error)) { - message = g_strdup_printf (_("Theme \"%s\" imported."), basename); - fe_message (message, FE_MSG_INFO); - g_free (message); + if (zoitechat_apply_theme (basename, &error)) + { + message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); + fe_message (message, FE_MSG_INFO); + handle_command (sess, "gui apply", FALSE); + g_free (message); + } + else + { + fe_message (error ? error->message : _("Theme imported, but failed to apply."), + FE_MSG_ERROR); + g_clear_error (&error); + } } else { @@ -796,9 +885,19 @@ irc_init (session *sess) if (zoitechat_import_theme (theme_path, &error)) { - message = g_strdup_printf (_("Theme \"%s\" imported."), basename); - fe_message (message, FE_MSG_INFO); - g_free (message); + if (zoitechat_apply_theme (basename, &error)) + { + message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); + fe_message (message, FE_MSG_INFO); + handle_command (sess, "gui apply", FALSE); + g_free (message); + } + else + { + fe_message (error ? error->message : _("Theme imported, but failed to apply."), + FE_MSG_ERROR); + g_clear_error (&error); + } } else { diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 810394d6..64e08bde 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -31,6 +31,7 @@ gboolean zoitechat_theme_path_from_arg (const char *arg, char **path_out); gboolean zoitechat_import_theme (const char *path, GError **error); +gboolean zoitechat_apply_theme (const char *theme_name, GError **error); #ifdef USE_OPENSSL #ifdef __APPLE__ diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c index 5345ab5f..f823fdc8 100644 --- a/src/fe-gtk/fe-gtk.c +++ b/src/fe-gtk/fe-gtk.c @@ -28,6 +28,7 @@ #ifdef WIN32 #include +#include #else #include #endif @@ -285,6 +286,8 @@ const char cursor_color_rc[] = "}" "widget \"*.zoitechat-inputbox\" style : application \"xc-ib-st\""; +InputStyle *create_input_style (InputStyle *style); + static const char adwaita_workaround_rc[] = "style \"zoitechat-input-workaround\"" "{" @@ -301,6 +304,85 @@ static const char adwaita_workaround_rc[] = "}" "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) { @@ -309,42 +391,16 @@ fe_system_prefers_dark (void) char *theme_name = NULL; #ifdef G_OS_WIN32 gboolean have_win_pref = FALSE; + + if (fe_win32_high_contrast_is_enabled ()) + return FALSE; #endif if (!settings) return FALSE; #ifdef G_OS_WIN32 - { - 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) - { - prefer_dark = (value == 0); - have_win_pref = TRUE; - } - } -#endif - -#ifdef G_OS_WIN32 + have_win_pref = fe_win32_try_get_system_dark (&prefer_dark); if (!have_win_pref) #endif if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), @@ -371,6 +427,46 @@ fe_system_prefers_dark (void) static gboolean auto_dark_mode_enabled = FALSE; +#ifdef G_OS_WIN32 +static void +fe_apply_windows_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); + } + +#if HAVE_GTK3 + { + static GtkCssProvider *win_theme_provider = NULL; + GdkScreen *screen = gdk_screen_get_default (); + const char *css = + "window.zoitechat-dark, .zoitechat-dark {" + "background-color: #202020;" + "color: #f0f0f0;" + "}" + "window.zoitechat-light, .zoitechat-light {" + "background-color: #f6f6f6;" + "color: #101010;" + "}"; + + if (!win_theme_provider) + win_theme_provider = gtk_css_provider_new (); + + gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL); + if (screen) + gtk_style_context_add_provider_for_screen ( + screen, + GTK_STYLE_PROVIDER (win_theme_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } +#endif +} +#endif + static void fe_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer data) { @@ -388,7 +484,7 @@ fe_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer da return; auto_dark_mode_enabled = enabled; - palette_apply_dark_mode (enabled); + fe_apply_theme_for_mode (ZOITECHAT_DARK_MODE_AUTO, NULL); setup_apply_real (0, TRUE, FALSE, FALSE); } @@ -404,6 +500,48 @@ 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); + 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); + + return enabled; +} + +void +fe_apply_theme_to_toplevel (GtkWidget *window) +{ + if (!window) + return; + +#if defined(G_OS_WIN32) && HAVE_GTK3 + { + GtkStyleContext *context = gtk_widget_get_style_context (window); + gboolean 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"); + } + } +#endif + + fe_win32_apply_native_titlebar (window, fe_dark_mode_is_enabled ()); +} + gboolean fe_dark_mode_is_enabled_for (unsigned int mode) { @@ -415,7 +553,7 @@ fe_dark_mode_is_enabled_for (unsigned int mode) return FALSE; case ZOITECHAT_DARK_MODE_AUTO: default: - return fe_system_prefers_dark (); + return auto_dark_mode_enabled; } } @@ -625,7 +763,11 @@ fe_init (void) GtkSettings *settings; palette_load (); - palette_apply_dark_mode (fe_dark_mode_is_enabled ()); + settings = gtk_settings_get_default (); + if (settings) + auto_dark_mode_enabled = fe_system_prefers_dark (); + + fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL); key_init (); pixmaps_init (); @@ -639,12 +781,10 @@ fe_init (void) input_style = create_input_style (gtk_style_new ()); #endif - settings = gtk_settings_get_default (); if (settings) { - auto_dark_mode_enabled = fe_system_prefers_dark (); g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme", - G_CALLBACK (fe_auto_dark_mode_changed), NULL); + G_CALLBACK (fe_auto_dark_mode_changed), NULL); g_signal_connect (settings, "notify::gtk-theme-name", G_CALLBACK (fe_auto_dark_mode_changed), NULL); } diff --git a/src/fe-gtk/fe-gtk.h b/src/fe-gtk/fe-gtk.h index 03e55508..626bfb2a 100644 --- a/src/fe-gtk/fe-gtk.h +++ b/src/fe-gtk/fe-gtk.h @@ -211,6 +211,8 @@ gboolean fe_dark_mode_is_enabled (void); gboolean fe_dark_mode_is_enabled_for (unsigned int mode); void fe_set_auto_dark_mode_state (gboolean enabled); void fe_refresh_auto_dark_mode (void); +gboolean fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed); +void fe_apply_theme_to_toplevel (GtkWidget *window); #define SPELL_ENTRY_GET_TEXT(e) ((char *)(gtk_entry_get_text (GTK_ENTRY(e)))) #define SPELL_ENTRY_SET_TEXT(e,txt) gtk_entry_set_text(GTK_ENTRY(e),txt) diff --git a/src/fe-gtk/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj index aa9bd954..f166ba03 100644 --- a/src/fe-gtk/fe-gtk.vcxproj +++ b/src/fe-gtk/fe-gtk.vcxproj @@ -1,4 +1,4 @@ - + v142 @@ -30,24 +30,24 @@ WIN32;NDEBUG;_WINDOWS;$(OwnFlags);%(PreprocessorDefinitions) - ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) + ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) 4244;%(DisableSpecificWarnings) $(DepsRoot)\lib;%(AdditionalLibraryDirectories) - $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;%(AdditionalDependencies) + $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;dwmapi.lib;%(AdditionalDependencies) mainCRTStartup WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;$(OwnFlags);%(PreprocessorDefinitions) - ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) + ..\common;$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories) 4244;4267;%(DisableSpecificWarnings) $(DepsRoot)\lib;%(AdditionalLibraryDirectories) - $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;%(AdditionalDependencies) + $(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;comsupp.lib;dwmapi.lib;%(AdditionalDependencies) mainCRTStartup diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index 5a59b876..ec5e6b6c 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -3944,6 +3944,7 @@ mg_create_topwindow (session *sess) mg_place_userlist_and_chanview (sess->gui); gtk_widget_show (win); + fe_apply_theme_to_toplevel (win); #ifdef G_OS_WIN32 parent_win = gtk_widget_get_window (win); @@ -4118,6 +4119,7 @@ mg_create_tabwindow (session *sess) mg_place_userlist_and_chanview (sess->gui); gtk_widget_show (win); + fe_apply_theme_to_toplevel (win); #ifdef G_OS_WIN32 parent_win = gtk_widget_get_window (win); @@ -4141,6 +4143,8 @@ mg_apply_setup (void) ((xtext_buffer *)sess->res->buffer)->needs_recalc = TRUE; if (!sess->gui->is_tab || !done_main) mg_place_userlist_and_chanview (sess->gui); + if (sess->gui->window) + fe_apply_theme_to_toplevel (sess->gui->window); if (sess->gui->is_tab) done_main = TRUE; list = list->next; diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index a845ab05..b66dfd73 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -77,6 +77,7 @@ zoitechat_gtk_ldflags = [] if host_machine.system() == 'windows' zoitechat_gtk_sources += 'notifications/notification-windows.c' + zoitechat_gtk_deps += cc.find_library('dwmapi', required: true) # TODO: mingw doesn't have these headers or libs # add_languages('cpp') diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 0402e27e..0825bc35 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -2045,22 +2045,6 @@ setup_theme_show_message (GtkMessageType message_type, const char *primary) gtk_widget_destroy (dialog); } -static gboolean -setup_theme_copy_file (const char *src, const char *dest, GError **error) -{ - GFile *src_file; - GFile *dest_file; - gboolean success; - - src_file = g_file_new_for_path (src); - dest_file = g_file_new_for_path (dest); - success = g_file_copy (src_file, dest_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error); - g_object_unref (src_file); - g_object_unref (dest_file); - - return success; -} - static void setup_theme_populate (setup_theme_ui *ui) { @@ -2138,11 +2122,6 @@ setup_theme_apply_cb (GtkWidget *button, gpointer user_data) GtkWidget *dialog; gint response; char *theme; - char *theme_dir = NULL; - char *colors_src = NULL; - char *colors_dest = NULL; - char *events_src = NULL; - char *events_dest = NULL; GError *error = NULL; theme = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (ui->combo)); @@ -2161,40 +2140,13 @@ setup_theme_apply_cb (GtkWidget *button, gpointer user_data) return; } - theme_dir = g_build_filename (get_xdir (), "themes", theme, NULL); - colors_src = g_build_filename (theme_dir, "colors.conf", NULL); - colors_dest = g_build_filename (get_xdir (), "colors.conf", NULL); - - if (!g_file_test (colors_src, G_FILE_TEST_IS_REGULAR)) - { - setup_theme_show_message (GTK_MESSAGE_ERROR, _("This theme is missing a colors.conf file.")); - goto cleanup; - } - - if (!setup_theme_copy_file (colors_src, colors_dest, &error)) + if (!zoitechat_apply_theme (theme, &error)) { setup_theme_show_message (GTK_MESSAGE_ERROR, error ? error->message : _("Failed to apply theme.")); g_clear_error (&error); goto cleanup; } - events_src = g_build_filename (theme_dir, "pevents.conf", NULL); - events_dest = g_build_filename (get_xdir (), "pevents.conf", NULL); - - if (g_file_test (events_src, G_FILE_TEST_IS_REGULAR)) - { - if (!setup_theme_copy_file (events_src, events_dest, &error)) - { - setup_theme_show_message (GTK_MESSAGE_ERROR, error ? error->message : _("Failed to apply event settings.")); - g_clear_error (&error); - goto cleanup; - } - } - else if (g_file_test (events_dest, G_FILE_TEST_EXISTS)) - { - g_unlink (events_dest); - } - palette_load (); palette_apply_dark_mode (fe_dark_mode_is_enabled ()); color_change = TRUE; @@ -2203,11 +2155,6 @@ setup_theme_apply_cb (GtkWidget *button, gpointer user_data) setup_theme_show_message (GTK_MESSAGE_INFO, _("Theme applied. Some changes may require a restart to take full effect.")); cleanup: - g_free (events_dest); - g_free (events_src); - g_free (colors_dest); - g_free (colors_src); - g_free (theme_dir); g_free (theme); } @@ -3059,7 +3006,9 @@ setup_apply (struct zoitechatprefs *pr) * the preference flips but the palette stays the same (aka: "nothing happens"). */ { - gboolean pal_changed = palette_apply_dark_mode (fe_dark_mode_is_enabled_for (prefs.hex_gui_dark_mode)); + gboolean pal_changed = FALSE; + + fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, &pal_changed); if (prefs.hex_gui_dark_mode != old_dark_mode || pal_changed) color_change = TRUE; } diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index de908181..02e334d5 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -195,6 +195,8 @@ static void gtk_xtext_search_textentry_fini (gpointer, gpointer); static void gtk_xtext_search_fini (xtext_buffer *); static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk_xtext_search_flags flags, GError **perr); static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp); +static gboolean gtk_xtext_word_select_char (const unsigned char *ch); +static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len); static inline void gtk_xtext_cursor_unref (GdkCursor *cursor) @@ -1856,10 +1858,12 @@ gtk_xtext_selection_draw (GtkXText * xtext, GdkEventMotion * event, gboolean ren if (xtext->word_select) { /* a word selection cannot be started if the cursor is out of bounds in gtk_xtext_button_press */ - gtk_xtext_get_word (xtext, low_x, low_y, NULL, &low_offs, NULL, NULL); + if (!gtk_xtext_get_word_select_range (xtext, low_x, low_y, NULL, &low_offs, NULL)) + gtk_xtext_get_word (xtext, low_x, low_y, NULL, &low_offs, NULL, NULL); /* in case the cursor is out of bounds we keep offset_end from gtk_xtext_find_char and fix the length */ - if (gtk_xtext_get_word (xtext, high_x, high_y, NULL, &high_offs, &high_len, NULL) == NULL) + if (!gtk_xtext_get_word_select_range (xtext, high_x, high_y, NULL, &high_offs, &high_len) && + gtk_xtext_get_word (xtext, high_x, high_y, NULL, &high_offs, &high_len, NULL) == NULL) high_len = high_offs == high_ent->str_len? 0: -1; /* -1 for the space, 0 if at the end */ high_offs += high_len; if (low_y < 0) @@ -2134,6 +2138,57 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, return word; } +static gboolean +gtk_xtext_word_select_char (const unsigned char *ch) +{ + gunichar uc; + + if (!ch || !*ch) + return FALSE; + + uc = g_utf8_get_char_validated ((const gchar *)ch, -1); + if (uc == (gunichar)-1 || uc == (gunichar)-2) + return FALSE; + return g_unichar_isalnum (uc) || uc == '_' || uc == '-'; +} + +static gboolean +gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len) +{ + textentry *ent; + int offset; + unsigned char *start, *end; + + ent = gtk_xtext_find_char (xtext, x, y, &offset, NULL); + if (!ent || offset < 0 || offset >= ent->str_len) + return FALSE; + + start = ent->str + offset; + end = g_utf8_find_next_char (start, ent->str + ent->str_len); + if (!gtk_xtext_word_select_char (start)) + return FALSE; + + while (start > ent->str) + { + unsigned char *prev = g_utf8_find_prev_char (ent->str, start); + if (!prev || !gtk_xtext_word_select_char (prev)) + break; + start = prev; + } + + while (end && end < ent->str + ent->str_len && gtk_xtext_word_select_char (end)) + end = g_utf8_find_next_char (end, ent->str + ent->str_len); + + if (ret_ent) + *ret_ent = ent; + if (ret_off) + *ret_off = (int)(start - ent->str); + if (ret_len) + *ret_len = (int)(end - start); + + return TRUE; +} + static void gtk_xtext_unrender_hilight (GtkXText *xtext) { @@ -2581,7 +2636,8 @@ gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event) if (event->type == GDK_2BUTTON_PRESS) /* WORD select */ { gtk_xtext_check_mark_stamp (xtext, mask); - if (gtk_xtext_get_word (xtext, x, y, &ent, &offset, &len, 0)) + if (gtk_xtext_get_word_select_range (xtext, x, y, &ent, &offset, &len) || + gtk_xtext_get_word (xtext, x, y, &ent, &offset, &len, 0)) { if (len == 0) return FALSE; @@ -4324,6 +4380,21 @@ gtk_xtext_nth (GtkXText *xtext, int line, int *subline) static int gtk_xtext_render_ents (GtkXText * xtext, textentry * enta, textentry * entb) { + /* + * On GTK3 (especially Wayland), event handlers are outside ::draw and direct + * window painting may not be presented immediately. Queue a frame instead so + * selections appear right away. + */ +#if HAVE_GTK3 + if (xtext->draw_cr == NULL) + { + GtkWidget *w = GTK_WIDGET (xtext); + if (gtk_widget_get_realized (w)) + gtk_widget_queue_draw (w); + return 0; + } +#endif + textentry *ent, *orig_ent, *tmp_ent; int line; int lines_max; @@ -5355,10 +5426,16 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp) g_source_remove (buf->xtext->io_tag); buf->xtext->io_tag = 0; } - buf->xtext->add_io_tag = g_timeout_add (REFRESH_TIMEOUT * 2, - (GSourceFunc) - gtk_xtext_render_page_timeout, - buf->xtext); + /* When at the bottom of the buffer, render immediately so long + * scrollback doesn't delay newly-sent messages appearing. + * Otherwise, keep idle batching to avoid extra redraws while + * scrolling around old content. */ + if (buf->scrollbar_down) + gtk_xtext_render_page_timeout (buf->xtext); + else + buf->xtext->add_io_tag = g_idle_add ((GSourceFunc) + gtk_xtext_render_page_timeout, + buf->xtext); } } if (buf->scrollbar_down) @@ -5390,6 +5467,7 @@ gtk_xtext_append_indent (xtext_buffer *buf, int space; int tempindent; int left_width; + int min_indent; if (left_len == -1) left_len = strlen (left_text); @@ -5428,24 +5506,32 @@ gtk_xtext_append_indent (xtext_buffer *buf, else space = 0; + min_indent = MARGIN + space; + /* do we need to auto adjust the separator position? */ if (buf->xtext->auto_indent && buf->indent < buf->xtext->max_auto_indent && - ent->indent < MARGIN + space) + ent->indent < min_indent) { - tempindent = MARGIN + space + buf->xtext->space_width + left_width; + tempindent = min_indent + buf->xtext->space_width + left_width; - if (tempindent > buf->indent) + /* Ignore tiny one-pixel style nudges. + * They can trigger expensive full-width recalculations and are + * perceived as a slight delay when sending messages with indenting on. */ + if (tempindent > buf->indent + buf->xtext->space_width) buf->indent = tempindent; if (buf->indent > buf->xtext->max_auto_indent) buf->indent = buf->xtext->max_auto_indent; - gtk_xtext_fix_indent (buf); - gtk_xtext_recalc_widths (buf, FALSE); + if (buf->indent > ent->indent + left_width + buf->xtext->space_width) + { + gtk_xtext_fix_indent (buf); + gtk_xtext_recalc_widths (buf, FALSE); - ent->indent = (buf->indent - left_width) - buf->xtext->space_width; - buf->xtext->force_render = TRUE; + ent->indent = (buf->indent - left_width) - buf->xtext->space_width; + buf->xtext->force_render = TRUE; + } } gtk_xtext_append_entry (buf, ent, stamp);