Compare commits

..
Author SHA1 Message Date
deepend b6da1b1082 macos: native editing shortcuts+resolve keybinding conflicts 2026-10-19 13:18:48 -06:00
deepend-tildeclub abf49ce756 Merge pull request #431 from ZoiteChat/dependabot/github_actions/actions/setup-python-7
ci: bump actions/setup-python from 6 to 7
2026-10-19 13:16:45 -06:00
deepend-tildeclub 6bf2f34fca Merge pull request #430 from ZoiteChat/dependabot/github_actions/actions/checkout-7
ci: bump actions/checkout from 5 to 7
2026-10-19 13:16:23 -06:00
deepend-tildeclub 2ea74498ed Merge pull request #429 from ZoiteChat/dependabot/github_actions/actions/upload-artifact-7
ci: bump actions/upload-artifact from 6 to 7
2026-10-19 13:16:05 -06:00
deepend-tildeclub 7004c679f2 Merge pull request #425 from juergbi/jbilleter/keybindings
Fix menu offsets to fix key bindings
2026-10-19 13:15:47 -06:00
deepend-tildeclub c9de3f2ce0 Merge pull request #428 from ZoiteChat/dependabot/submodules/flatpak/shared-modules-a58e654
Bump flatpak/shared-modules from `55a86b3` to `a58e654`
2026-10-19 12:56:58 -06:00
dependabot[bot] f77d4f6d59 ci: bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 18:38:31 +00:00
dependabot[bot] 89b9a32168 ci: bump actions/checkout from 5 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 18:38:27 +00:00
dependabot[bot] 357b766369 ci: bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 18:38:23 +00:00
deepend 45a8ba18a6 expand Dependabot coverage and group updates 2026-10-19 12:36:50 -06:00
dependabot[bot] 0e0f8f72b2 Bump flatpak/shared-modules from 55a86b3 to a58e654
Bumps [flatpak/shared-modules](https://github.com/flathub/shared-modules) from `55a86b3` to `a58e654`.
- [Commits](https://github.com/flathub/shared-modules/compare/55a86b3da4969984f7de8fb02b3370247c9a266d...a58e654b9113feffaee113cc7167ebbbdb406bee)

---
updated-dependencies:
- dependency-name: flatpak/shared-modules
  dependency-version: a58e654b9113feffaee113cc7167ebbbdb406bee
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 18:36:21 +00:00
deepend dc5f8f7631 automate shared-modules updates 2026-10-19 12:21:11 -06:00
Jürg Billeter 8a03585611 Fix menu offsets to fix key bindings
Key bindings for `copy-selection`, `search-text` and others weren't
working due to incorrect offsets.
2026-10-16 22:14:35 +02:00
18 changed files with 829 additions and 24 deletions
+37
View File
@@ -0,0 +1,37 @@
version: 2
updates:
# Flatpak shared modules
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 2
labels:
- "dependencies"
# GitHub Actions dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 3
labels:
- "dependencies"
commit-message:
prefix: "ci"
cooldown:
default-days: 7
groups:
actions-minor-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
artifact-metadata: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
submodules: true
@@ -312,7 +312,7 @@ jobs:
subject-path: Zoitechat-*-x86_64.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: zoitechat-appimage
path: Zoitechat-*-x86_64.AppImage
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
options: --privileged
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
submodules: true
@@ -39,7 +39,7 @@ jobs:
- name: Upload Flatpak Bundle
id: upload_flatpak
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: zoitechat.flatpak
path: zoitechat.flatpak
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install native dependencies
run: brew bundle --file packaging/macos/Brewfile
- name: Test packaging logic
@@ -112,7 +112,7 @@ jobs:
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate release settings before building
+5 -5
View File
@@ -26,9 +26,9 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: '3.14.3'
architecture: ${{ matrix.arch }}
@@ -138,7 +138,7 @@ jobs:
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
}
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: '3.8.10'
architecture: ${{ matrix.arch }}
@@ -222,7 +222,7 @@ jobs:
- name: Upload Installer
id: upload_installer
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Installer ${{ matrix.arch }}
path: ZoiteChat-*.exe
@@ -236,7 +236,7 @@ jobs:
- name: Upload Build Files
id: upload_buildfiles
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Build Files ${{ matrix.arch }}
path: zoitechat-build
+5 -5
View File
@@ -27,9 +27,9 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: '3.14.3'
architecture: ${{ matrix.arch }}
@@ -146,7 +146,7 @@ jobs:
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
}
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: '3.8.10'
architecture: ${{ matrix.arch }}
@@ -399,7 +399,7 @@ jobs:
- name: Upload MSI Installer
id: upload_msi
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: MSI Installer ${{ matrix.arch }}
path: ${{ steps.build_msi.outputs.path }}
@@ -414,7 +414,7 @@ jobs:
- name: Upload MSIX Package
id: upload_msix
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build_msix.outputs.artifact_name }}
path: ${{ steps.build_msix.outputs.path }}
+42
View File
@@ -49,6 +49,48 @@ sources and build them in a controlled prefix, or maintain pinned Homebrew formu
## 2. Verify portability and behavior
### Keyboard shortcuts
These defaults apply only to macOS; Linux and Windows shortcuts are unchanged.
| Shortcut | Action |
| --- | --- |
| Command+C / X / V | Copy / cut / paste in text fields, including search and preferences |
| Command+A | Select all text in the focused field |
| Command+Left / Right | Move to the beginning / end of the line |
| Option+Left / Right | Move by words |
| Command+Up / Down | Move to the beginning / end of the text |
| Shift with the movement shortcuts above | Extend the selection |
| Option+Delete (Backspace) | Delete back to the beginning of the word |
| Command+Delete (Backspace) | Delete back to the beginning of the line |
| Command+, | Open Preferences |
| Command+M | Minimize the active window |
| Control+Tab | Completion in the opposite direction; Tab / Shift+Tab still complete normally |
| Command+Option+Left / Right | Move the current tab left / right |
| Command+Shift+R | Reset the chat marker; Command+Shift+M still moves to the marker |
The inherited Control+C/X/V clipboard bindings are removed on macOS only.
Command+C copies selected input text first; if no input text is selected, it
copies the selected chat text. Command+X cuts editable text, never messages
from chat history. Password protection and read-only fields retain GTK's
existing behavior. Undo/redo is not added: GTK3's entry widget needs a separate
editing-history implementation before Command+Z can work reliably.
Exact old default assignments for Command+Tab, Option+Left/Right and Command+M
are remapped in memory when loading defaults or an existing configuration.
Other custom bindings are preserved. If a replacement key is already assigned,
both existing assignments are left alone; resolve that conflict in **Settings
→ Keyboard Shortcuts**. Saving there persists the updated assignments.
Command+Tab is left to macOS for switching applications. Plugins and custom
input bindings retain priority; the new native menu accelerators are omitted
when the same key is already assigned to an input action.
Before distributing a build, test the shortcuts in message input, search,
preferences, a detached chat window and a multiline editor, including Unicode,
selection replacement, and read-only/password fields. The Meson widget tests
exercise the GTK actions; the screenshot smoke check does not send real keys
through the macOS event loop.
```sh
python3 -m unittest discover -s packaging/macos/tests -v
python3 packaging/macos/bundle.py audit dist/ZoiteChat.app
+3
View File
@@ -23,6 +23,7 @@
#include "fe-gtk.h"
#ifdef __APPLE__
#include "../common/macos.h"
#include "macos-shortcuts.h"
#endif
#ifdef GDK_WINDOWING_WIN32
@@ -503,6 +504,8 @@ fe_args (int argc, char *argv[])
gtk_init (&argc, &argv);
#ifdef __APPLE__
macos_shortcuts_init (setup_open);
if (g_getenv ("ZOITECHAT_MACOS_SHARE"))
{
char *icons = g_build_filename (g_getenv ("ZOITECHAT_MACOS_SHARE"), "icons", NULL);
+63
View File
@@ -55,6 +55,9 @@
#include "maingui.h"
#include "textgui.h"
#include "fkeys.h"
#ifdef __APPLE__
#include "macos-shortcuts.h"
#endif
#define ICON_FKEYS_NEW "document-new"
#define ICON_FKEYS_DELETE "edit-delete"
@@ -570,6 +573,12 @@ key_handle_key_press (GtkWidget *wid, GdkEventKey *evt, session *sess)
list = g_slist_next (list);
}
#ifdef __APPLE__
/* Plugins and user-defined input shortcuts above retain priority. */
if (macos_shortcuts_copy_chat (wid, evt, GTK_XTEXT (sess->gui->xtext)))
return TRUE;
#endif
switch (evt->keyval)
{
case GDK_KEY_space:
@@ -1333,6 +1342,57 @@ key_save_kbs (void)
#define KBSTATE_DT1 3
#define KBSTATE_DT2 4
#ifdef __APPLE__
static void
key_macos_adjust_defaults (GSList *bindings)
{
GSList *item, *other;
struct key_binding *kb, *existing;
guint keyval;
GdkModifierType mod;
for (item = bindings; item; item = item->next)
{
kb = item->data;
keyval = kb->keyval;
mod = kb->mod;
if (!macos_shortcuts_remap_default (&keyval, &mod,
key_actions[kb->action].name,
kb->data1, kb->data2))
continue;
/* Never let a migrated default shadow an existing user shortcut.
* In that case leave the old assignment for the user to resolve. */
for (other = bindings; other; other = other->next)
{
existing = other->data;
if (existing != kb &&
gdk_keyval_to_lower (existing->keyval) == gdk_keyval_to_lower (keyval) &&
existing->mod == mod)
break;
}
if (!other)
{
kb->keyval = keyval;
kb->mod = mod;
}
}
}
gboolean
key_is_bound (guint keyval, GdkModifierType mod)
{
GSList *item;
for (item = keybind_list; item; item = item->next)
{
struct key_binding *kb = item->data;
if (kb->keyval == keyval && kb->mod == mod)
return TRUE;
}
return FALSE;
}
#endif
#define STRIP_WHITESPACE \
while (buf[0] == ' ' || buf[0] == '\t') \
buf++; \
@@ -1519,6 +1579,9 @@ key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list)
}
}
g_free (ibuf);
#ifdef __APPLE__
key_macos_adjust_defaults (list);
#endif
*out_list = list;
return 0;
+3
View File
@@ -36,5 +36,8 @@ int key_action_insert (GtkWidget * wid, GdkEventKey * evt, char *d1, char *d2,
session *sess);
void key_check_replace_on_change (GtkEditable *editable, gpointer data);
gboolean key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod);
#ifdef __APPLE__
gboolean key_is_bound (guint keyval, GdkModifierType mod);
#endif
#endif
+243
View File
@@ -0,0 +1,243 @@
/* macOS text editing shortcuts. GPL-2.0-or-later; see COPYING. */
#include "macos-shortcuts.h"
#include "config.h"
#include "../common/zoitechat.h"
#include "xtext.h"
#include <gdk/gdkkeysyms.h>
static void (*preferences_callback) (void);
static void
open_preferences (GtkWindow *window)
{
if (preferences_callback)
preferences_callback ();
}
static void
minimize_window (GtkWindow *window)
{
gtk_window_iconify (window);
}
void
macos_shortcuts_minimize_active (GtkWindow *fallback)
{
GList *windows = gtk_window_list_toplevels ();
GList *item;
GtkWindow *target = fallback;
for (item = windows; item; item = item->next)
{
if (gtk_window_is_active (GTK_WINDOW (item->data)))
{
target = GTK_WINDOW (item->data);
break;
}
}
if (target)
minimize_window (target);
g_list_free (windows);
}
static void
add_movement (GtkBindingSet *bindings, guint keyval, GdkModifierType mod,
GtkMovementStep step, gint count)
{
gtk_binding_entry_add_signal (bindings, keyval, mod, "move-cursor", 3,
GTK_TYPE_MOVEMENT_STEP, step,
G_TYPE_INT, count, G_TYPE_BOOLEAN, FALSE);
gtk_binding_entry_add_signal (bindings, keyval, mod | GDK_SHIFT_MASK,
"move-cursor", 3, GTK_TYPE_MOVEMENT_STEP, step,
G_TYPE_INT, count, G_TYPE_BOOLEAN, TRUE);
}
static void
delete_text_view_backwards (GtkTextView *view, gboolean word)
{
GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
GtkTextIter start, end;
gboolean editable = gtk_text_view_get_editable (view);
/* GTK3 leaves GTK_DELETE_DISPLAY_LINE_ENDS unimplemented in TextView.
* Preserve selection deletion and per-range editability instead of
* moving the cursor first or deleting protected text unconditionally. */
if (gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
gtk_text_buffer_delete_selection (buffer, TRUE, editable);
else
{
gtk_text_buffer_get_iter_at_mark (buffer, &end, gtk_text_buffer_get_insert (buffer));
start = end;
if (word)
gtk_text_iter_backward_word_start (&start);
else
gtk_text_view_backward_display_line_start (view, &start);
gtk_text_buffer_begin_user_action (buffer);
gtk_text_buffer_delete_interactive (buffer, &start, &end, editable);
gtk_text_buffer_end_user_action (buffer);
}
gtk_text_view_reset_im_context (view);
gtk_text_view_scroll_mark_onscreen (view, gtk_text_buffer_get_insert (buffer));
}
static void
delete_display_line_start (GtkTextView *view)
{
delete_text_view_backwards (view, FALSE);
}
static void
delete_word_start (GtkTextView *view)
{
delete_text_view_backwards (view, TRUE);
}
void
macos_shortcuts_init (void (*callback) (void))
{
GType types[] = {GTK_TYPE_ENTRY, GTK_TYPE_TEXT_VIEW};
gpointer window_class;
GtkBindingSet *window_bindings;
static gboolean initialized = FALSE;
guint i;
preferences_callback = callback;
if (initialized)
return;
initialized = TRUE;
/* GTK's built-in editing bindings use Control, even with Quartz.
* Use Command for these three actions on macOS, preserving GTK's
* selection, password-field and read-only checks. Entry subclasses
* (including the spell-checking input and search fields) inherit these. */
for (i = 0; i < G_N_ELEMENTS (types); i++)
{
gpointer klass = g_type_class_ref (types[i]);
GtkBindingSet *bindings = gtk_binding_set_by_class (klass);
gtk_binding_entry_remove (bindings, GDK_KEY_c, GDK_CONTROL_MASK);
gtk_binding_entry_remove (bindings, GDK_KEY_x, GDK_CONTROL_MASK);
gtk_binding_entry_remove (bindings, GDK_KEY_v, GDK_CONTROL_MASK);
gtk_binding_entry_add_signal (bindings, GDK_KEY_c, GDK_META_MASK,
"copy-clipboard", 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_x, GDK_META_MASK,
"cut-clipboard", 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_v, GDK_META_MASK,
"paste-clipboard", 0);
/* Two movements select the whole buffer without changing its text. */
gtk_binding_entry_add_signal (bindings, GDK_KEY_a, GDK_META_MASK,
"move-cursor", 3, GTK_TYPE_MOVEMENT_STEP,
GTK_MOVEMENT_BUFFER_ENDS, G_TYPE_INT, -1,
G_TYPE_BOOLEAN, FALSE);
gtk_binding_entry_add_signal (bindings, GDK_KEY_a, GDK_META_MASK,
"move-cursor", 3, GTK_TYPE_MOVEMENT_STEP,
GTK_MOVEMENT_BUFFER_ENDS, G_TYPE_INT, 1,
G_TYPE_BOOLEAN, TRUE);
add_movement (bindings, GDK_KEY_Left, GDK_META_MASK,
GTK_MOVEMENT_DISPLAY_LINE_ENDS, -1);
add_movement (bindings, GDK_KEY_Right, GDK_META_MASK,
GTK_MOVEMENT_DISPLAY_LINE_ENDS, 1);
add_movement (bindings, GDK_KEY_Left, GDK_MOD1_MASK,
GTK_MOVEMENT_WORDS, -1);
add_movement (bindings, GDK_KEY_Right, GDK_MOD1_MASK,
GTK_MOVEMENT_WORDS, 1);
add_movement (bindings, GDK_KEY_Up, GDK_META_MASK,
GTK_MOVEMENT_BUFFER_ENDS, -1);
add_movement (bindings, GDK_KEY_Down, GDK_META_MASK,
GTK_MOVEMENT_BUFFER_ENDS, 1);
if (types[i] == GTK_TYPE_TEXT_VIEW)
{
g_signal_new_class_handler ("zoitechat-macos-delete-word-start", types[i],
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (delete_word_start),
NULL, NULL, NULL, G_TYPE_NONE, 0);
g_signal_new_class_handler ("zoitechat-macos-delete-line-start", types[i],
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (delete_display_line_start),
NULL, NULL, NULL, G_TYPE_NONE, 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_META_MASK,
"zoitechat-macos-delete-line-start", 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_MOD1_MASK,
"zoitechat-macos-delete-word-start", 0);
}
else
{
gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_MOD1_MASK,
"delete-from-cursor", 2, GTK_TYPE_DELETE_TYPE,
GTK_DELETE_WORD_ENDS, G_TYPE_INT, -1);
gtk_binding_entry_add_signal (bindings, GDK_KEY_BackSpace, GDK_META_MASK,
"delete-from-cursor", 2, GTK_TYPE_DELETE_TYPE,
GTK_DELETE_DISPLAY_LINE_ENDS, G_TYPE_INT, -1);
}
g_type_class_unref (klass);
}
/* Window-level bindings also work in dialogs without a main menu bar.
* The native menu uses the same actions when Quartz handles the key. */
window_class = g_type_class_ref (GTK_TYPE_WINDOW);
window_bindings = gtk_binding_set_by_class (window_class);
g_signal_new_class_handler ("zoitechat-macos-minimize", GTK_TYPE_WINDOW,
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (minimize_window), NULL, NULL, NULL,
G_TYPE_NONE, 0);
g_signal_new_class_handler ("zoitechat-macos-preferences", GTK_TYPE_WINDOW,
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (open_preferences), NULL, NULL, NULL,
G_TYPE_NONE, 0);
gtk_binding_entry_add_signal (window_bindings, GDK_KEY_m, GDK_META_MASK,
"zoitechat-macos-minimize", 0);
gtk_binding_entry_add_signal (window_bindings, GDK_KEY_comma, GDK_META_MASK,
"zoitechat-macos-preferences", 0);
g_type_class_unref (window_class);
}
gboolean
macos_shortcuts_remap_default (guint *keyval, GdkModifierType *mod,
const char *action, const char *data1,
const char *data2)
{
/* Only exact legacy defaults qualify. Custom actions, parameters and
* alternate shortcuts must survive loading a configuration unchanged. */
if (data2 && *data2)
return FALSE;
if (*keyval == GDK_KEY_Tab && *mod == GDK_META_MASK &&
!g_strcmp0 (action, "Complete nick/command") && !g_strcmp0 (data1, "Up"))
{
*mod = GDK_CONTROL_MASK;
return TRUE;
}
if ((!data1 || !*data1) && *mod == GDK_MOD1_MASK &&
((*keyval == GDK_KEY_Left && !g_strcmp0 (action, "Move front tab left")) ||
(*keyval == GDK_KEY_Right && !g_strcmp0 (action, "Move front tab right"))))
{
*mod |= GDK_META_MASK;
return TRUE;
}
if (*keyval == GDK_KEY_m && *mod == GDK_META_MASK &&
!g_strcmp0 (action, "Menu Shortcut") && !g_strcmp0 (data1, "reset-marker"))
{
*keyval = GDK_KEY_R;
*mod |= GDK_SHIFT_MASK;
return TRUE;
}
return FALSE;
}
gboolean
macos_shortcuts_copy_chat (GtkWidget *entry, GdkEventKey *event, GtkXText *xtext)
{
gint start, end;
GdkModifierType modifiers = event->state &
(GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK |
GDK_META_MASK | GDK_SUPER_MASK | GDK_HYPER_MASK);
/* XText leaves keyboard focus in the message input. Prefer an input
* selection; otherwise Command+C copies the selected chat text. Leave
* Command+Shift+C and all other shortcuts to their existing handlers. */
if (gdk_keyval_to_lower (event->keyval) != GDK_KEY_c || modifiers != GDK_META_MASK ||
gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start, &end))
return FALSE;
gtk_xtext_copy_selection (xtext);
return TRUE;
}
+17
View File
@@ -0,0 +1,17 @@
/* macOS text editing shortcuts. GPL-2.0-or-later; see COPYING. */
#ifndef ZOITECHAT_MACOS_SHORTCUTS_H
#define ZOITECHAT_MACOS_SHORTCUTS_H
#include <gtk/gtk.h>
typedef struct _GtkXText GtkXText;
void macos_shortcuts_init (void (*open_preferences) (void));
void macos_shortcuts_minimize_active (GtkWindow *fallback);
gboolean macos_shortcuts_remap_default (guint *keyval, GdkModifierType *mod,
const char *action, const char *data1,
const char *data2);
gboolean macos_shortcuts_copy_chat (GtkWidget *entry, GdkEventKey *event,
GtkXText *xtext);
#endif
+59 -6
View File
@@ -51,6 +51,9 @@
#include "chanlist.h"
#include "editlist.h"
#include "fkeys.h"
#ifdef __APPLE__
#include "macos-shortcuts.h"
#endif
#include "gtkutil.h"
#include "maingui.h"
#include "notifygui.h"
@@ -1322,6 +1325,17 @@ menu_settings (GtkWidget * wid, gpointer none)
setup_open ();
}
#ifdef __APPLE__
static void
menu_macos_minimize (GtkWidget *widget, gpointer userdata)
{
GtkWindow *fallback = NULL;
if (current_sess && current_sess->gui && current_sess->gui->window)
fallback = GTK_WINDOW (current_sess->gui->window);
macos_shortcuts_minimize_active (fallback);
}
#endif
static void
menu_usermenu (void)
{
@@ -2069,7 +2083,7 @@ static struct mymenu mymenu[] = {
{N_("_Copy Selection"), menu_copy_selection, 0, M_MENUITEM, 0, 0, 1},
{N_("C_lear Text"), menu_flushbuffer, 0, M_MENUITEM, 0, 0, 1},
{N_("Save Text" ELLIPSIS), menu_savebuffer, 0, M_MENUITEM, 0, 0, 1},
#define SEARCH_OFFSET (70)
#define SEARCH_OFFSET (71)
{N_("Search"), 0, 0, M_MENUSUB, 0, 0, 1},
{N_("Search Text" ELLIPSIS), menu_search, 0, M_MENUITEM, 0, 0, 1},
{N_("Search Next" ), menu_search_next, 0, M_MENUITEM, 0, 0, 1},
@@ -2106,11 +2120,11 @@ menu_get_key_action_name (int index)
return "fullscreen-toggle";
case AWAY_OFFSET:
return "away-toggle";
case 65:
return "reset-marker";
case 66:
return "move-marker";
return "reset-marker";
case 67:
return "move-marker";
case 68:
return "copy-selection";
case SEARCH_OFFSET + 1:
return "search-text";
@@ -2118,7 +2132,7 @@ menu_get_key_action_name (int index)
return "search-next";
case SEARCH_OFFSET + 3:
return "search-previous";
case 75:
case 77:
return "contents";
}
@@ -2142,6 +2156,20 @@ menu_add_keybinding_accel (GtkWidget *item, GtkAccelGroup *accel_group, const ch
g_object_set_data (G_OBJECT (item), "zc-key-accel-mod", GUINT_TO_POINTER (mod));
}
#ifdef __APPLE__
static void
menu_add_macos_accel (GtkWidget *item, GtkAccelGroup *accel_group, guint keyval)
{
g_object_set_data (G_OBJECT (item), "zc-macos-default-key", GUINT_TO_POINTER (keyval));
if (!accel_group || key_is_bound (keyval, GDK_META_MASK))
return;
gtk_widget_add_accelerator (item, "activate", accel_group, keyval,
GDK_META_MASK, GTK_ACCEL_VISIBLE);
g_object_set_data (G_OBJECT (item), "zc-key-accel-key", GUINT_TO_POINTER (keyval));
g_object_set_data (G_OBJECT (item), "zc-key-accel-mod", GUINT_TO_POINTER (GDK_META_MASK));
}
#endif
static void
menu_refresh_keybinding_accels (GtkWidget *widget, gpointer data)
{
@@ -2164,6 +2192,12 @@ menu_refresh_keybinding_accels (GtkWidget *widget, gpointer data)
}
name = g_object_get_data (G_OBJECT (widget), "zc-key-action");
#ifdef __APPLE__
keyval = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), "zc-macos-default-key"));
if (keyval)
menu_add_macos_accel (widget, accel_group, keyval);
else
#endif
menu_add_keybinding_accel (widget, accel_group, name);
submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget));
@@ -2217,7 +2251,12 @@ menu_key_action (const char *name, guint keyval, GdkModifierType state)
menu_close (NULL, NULL);
else if (!strcmp (name, "quit"))
{
if (!prefs.hex_gui_ctrlq_quit && keyval == GDK_KEY_q && (state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)) == STATE_CTRL)
if (!prefs.hex_gui_ctrlq_quit && keyval == GDK_KEY_q &&
(state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK
#ifdef __APPLE__
| GDK_META_MASK
#endif
)) == STATE_CTRL)
return FALSE;
menu_quit (NULL, NULL);
}
@@ -2841,6 +2880,16 @@ menu_create_main (void *accel_group, int bar, int away, int toplevel,
#endif
gtk_menu_shell_append (GTK_MENU_SHELL (menu_bar), menu_item);
gtk_widget_show (menu_item);
#ifdef __APPLE__
if (bar && !strcmp (mymenu[i].text, "_Window"))
{
GtkWidget *minimize = gtk_menu_item_new_with_mnemonic (_("_Minimize"));
menu_add_macos_accel (minimize, accel_group, GDK_KEY_m);
g_signal_connect (minimize, "activate", G_CALLBACK (menu_macos_minimize), NULL);
gtk_menu_shell_append (GTK_MENU_SHELL (menu), minimize);
gtk_widget_show (minimize);
}
#endif
break;
case M_MENUPIX:
@@ -2854,6 +2903,10 @@ menu_create_main (void *accel_group, int bar, int away, int toplevel,
case M_MENUITEM:
item = gtk_menu_item_new_with_mnemonic (_(mymenu[i].text));
normalitem:
#ifdef __APPLE__
if (bar && mymenu[i].callback == menu_settings)
menu_add_macos_accel (item, accel_group, GDK_KEY_comma);
#endif
g_object_set_data (G_OBJECT (item), "zc-key-action", (gpointer) menu_get_key_action_name (i));
menu_add_keybinding_accel (item, accel_group, menu_get_key_action_name (i));
if (mymenu[i].key != 0 && !(mymenu[i].id == MENU_ID_QUIT && !prefs.hex_gui_ctrlq_quit))
+2 -1
View File
@@ -109,6 +109,7 @@ if host_machine.system() == 'windows'
elif host_machine.system() == 'darwin'
add_languages('objc', required: true)
zoitechat_gtk_sources += 'macos-shortcuts.c'
zoitechat_gtk_sources += 'notifications/notification-osx.m'
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications'])
zoitechat_gtk_objcflags += '-fobjc-arc'
@@ -296,7 +297,7 @@ test('Emoji Font Tests', emoji_font_tests,
)
xtext_tests = executable('xtext_tests',
['tests/test-xtext.c', '../common/hexcolor.c'],
['tests/test-xtext.c', 'macos-shortcuts.c', '../common/hexcolor.c'],
include_directories: [config_h_include],
dependencies: [gtk_dep],
)
+4
View File
@@ -567,7 +567,11 @@ static const setting general_settings[] =
{ST_TOGGLE, N_("Hide join and part messages"), P_OFFINTNL(hex_irc_conf_mode), N_("Hide channel join/part messages by default."), 0, 0},
{ST_TOGGLE, N_("Hide nick change messages"), P_OFFINTNL(hex_irc_hide_nickchange), 0, 0, 0},
{ST_TOGGLE, N_("Hide hostmasks in join and part messages"), P_OFFINTNL(hex_irc_hide_join_part_hostmask), 0, 0, 0},
#ifdef __APPLE__
{ST_TOGGLE, N_("Enable Command+Q to quit"), P_OFFINTNL(hex_gui_ctrlq_quit), 0, 0, 0},
#else
{ST_TOGGLE, N_("Enable Ctrl+Q to quit"), P_OFFINTNL(hex_gui_ctrlq_quit), 0, 0, 0},
#endif
{ST_END, 0, 0, 0, 0, 0}
};
+1
View File
@@ -23,5 +23,6 @@
#include "theme/theme-manager.h"
void setup_apply_real (const ThemeChangedEvent *event);
void setup_open (void);
#endif
+338
View File
@@ -3,6 +3,7 @@
* adding test-only symbols to the widget's public API. Application services
* are stubbed; GTK, Pango, Cairo and the widget itself are exercised normally. */
#include "../xtext.c"
#include "../macos-shortcuts.h"
#include <glib/gstdio.h>
struct zoitechatprefs prefs;
@@ -252,6 +253,333 @@ static void test_selection_and_clipboard (Fixture *f, gconstpointer data)
g_assert_false (gtk_xtext_is_selecting (f->xtext));
}
static void assert_clipboard (GtkWidget *widget, const char *expected)
{
char *text = gtk_clipboard_wait_for_text (
gtk_widget_get_clipboard (widget, GDK_SELECTION_CLIPBOARD));
g_assert_cmpstr (text, ==, expected);
g_free (text);
}
static GtkWidget *shortcut_window (GtkWidget *widget)
{
GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_container_add (GTK_CONTAINER (window), widget);
gtk_widget_show_all (window);
drain_events ();
return window;
}
static void test_macos_entry_shortcuts (void)
{
/* SearchEntry also checks that subclasses inherit the new bindings. */
GtkWidget *entry = g_object_ref_sink (gtk_search_entry_new ());
GtkWidget *window = shortcut_window (entry);
GtkClipboard *clipboard = gtk_widget_get_clipboard (entry, GDK_SELECTION_CLIPBOARD);
gint64 deadline;
gtk_entry_set_text (GTK_ENTRY (entry), "hello 猫");
gtk_editable_select_region (GTK_EDITABLE (entry), 6, 7);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_c, GDK_META_MASK));
assert_clipboard (entry, "猫");
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "hello 猫");
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_x, GDK_META_MASK));
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "hello ");
assert_clipboard (entry, "猫");
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_v, GDK_META_MASK));
deadline = g_get_monotonic_time () + G_TIME_SPAN_SECOND * 2;
while (strcmp (gtk_entry_get_text (GTK_ENTRY (entry)), "hello 猫") &&
g_get_monotonic_time () < deadline)
{
drain_events ();
g_usleep (1000);
}
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "hello 猫");
/* The old Control actions must no longer perform clipboard editing. */
gtk_editable_select_region (GTK_EDITABLE (entry), 0, -1);
gtk_clipboard_set_text (clipboard, "sentinel", -1);
g_assert_false (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_c, GDK_CONTROL_MASK));
g_assert_false (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_x, GDK_CONTROL_MASK));
g_assert_false (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_v, GDK_CONTROL_MASK));
assert_clipboard (entry, "sentinel");
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "hello 猫");
/* Use GTK actions so password protection and editability still apply. */
gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_c, GDK_META_MASK));
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_x, GDK_META_MASK));
assert_clipboard (entry, "sentinel");
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "hello 猫");
gtk_entry_set_visibility (GTK_ENTRY (entry), TRUE);
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_x, GDK_META_MASK));
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_v, GDK_META_MASK));
drain_events ();
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "hello 猫");
gtk_widget_destroy (window);
g_object_unref (entry);
}
static void test_macos_textview_shortcuts (void)
{
GtkWidget *view = g_object_ref_sink (gtk_text_view_new ());
GtkWidget *window = shortcut_window (view);
GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
GtkTextIter start, end;
gint64 deadline;
gtk_text_buffer_set_text (buffer, "two\nlines", -1);
gtk_text_buffer_get_bounds (buffer, &start, &end);
gtk_text_buffer_select_range (buffer, &start, &end);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_c, GDK_META_MASK));
assert_clipboard (view, "two\nlines");
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_x, GDK_META_MASK));
g_assert_cmpint (gtk_text_buffer_get_char_count (buffer), ==, 0);
assert_clipboard (view, "two\nlines");
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_v, GDK_META_MASK));
deadline = g_get_monotonic_time () + G_TIME_SPAN_SECOND * 2;
while (gtk_text_buffer_get_char_count (buffer) == 0 &&
g_get_monotonic_time () < deadline)
{
drain_events ();
g_usleep (1000);
}
g_assert_cmpint (gtk_text_buffer_get_char_count (buffer), ==, 9);
gtk_text_buffer_get_bounds (buffer, &start, &end);
{
char *text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
g_assert_cmpstr (text, ==, "two\nlines");
g_free (text);
}
g_assert_false (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_c, GDK_CONTROL_MASK));
g_assert_false (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_x, GDK_CONTROL_MASK));
g_assert_false (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_v, GDK_CONTROL_MASK));
gtk_widget_destroy (window);
g_object_unref (view);
}
static void test_macos_chat_copy (Fixture *f, gconstpointer data)
{
GtkWidget *entry = g_object_ref_sink (gtk_entry_new ());
GtkWidget *window = shortcut_window (entry);
GdkEventKey event = {0};
append (f, "chat selection 猫");
select_entries (f, f->xtext->buffer->text_first, f->xtext->buffer->text_last);
event.keyval = GDK_KEY_C; /* Caps Lock must not disable chat copying. */
event.state = GDK_META_MASK | GDK_LOCK_MASK | GDK_MOD2_MASK;
g_assert_true (macos_shortcuts_copy_chat (entry, &event, f->xtext));
assert_clipboard (entry, "chat selection 猫");
/* Selected input takes priority over an older selection in scrollback. */
gtk_entry_set_text (GTK_ENTRY (entry), "input");
gtk_editable_select_region (GTK_EDITABLE (entry), 0, -1);
g_assert_false (macos_shortcuts_copy_chat (entry, &event, f->xtext));
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_c, GDK_META_MASK));
assert_clipboard (entry, "input");
gtk_editable_select_region (GTK_EDITABLE (entry), 0, 0);
event.state = GDK_META_MASK | GDK_SHIFT_MASK;
g_assert_false (macos_shortcuts_copy_chat (entry, &event, f->xtext));
event.state = GDK_CONTROL_MASK;
g_assert_false (macos_shortcuts_copy_chat (entry, &event, f->xtext));
event.state = GDK_META_MASK;
event.keyval = GDK_KEY_x;
g_assert_false (macos_shortcuts_copy_chat (entry, &event, f->xtext));
assert_clipboard (entry, "input");
gtk_widget_destroy (window);
g_object_unref (entry);
}
static void assert_entry_selection (GtkWidget *entry, gint start, gint end)
{
gint actual_start, actual_end;
g_assert_true (gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
&actual_start, &actual_end));
g_assert_cmpint (actual_start, ==, start);
g_assert_cmpint (actual_end, ==, end);
}
static void test_macos_entry_editing (void)
{
GtkWidget *entry = gtk_entry_new ();
GtkWidget *window = shortcut_window (entry);
gtk_entry_set_text (GTK_ENTRY (entry), "one two three");
gtk_editable_set_position (GTK_EDITABLE (entry), 5);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_a, GDK_META_MASK));
assert_entry_selection (entry, 0, 13);
gtk_editable_set_position (GTK_EDITABLE (entry), 5);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Left, GDK_META_MASK));
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (entry)), ==, 0);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Right, GDK_META_MASK));
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (entry)), ==, 13);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Left, GDK_META_MASK | GDK_SHIFT_MASK));
assert_entry_selection (entry, 0, 13);
gtk_editable_set_position (GTK_EDITABLE (entry), 13);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Left, GDK_MOD1_MASK));
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (entry)), ==, 8);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Left, GDK_MOD1_MASK | GDK_SHIFT_MASK));
assert_entry_selection (entry, 4, 8);
gtk_editable_set_position (GTK_EDITABLE (entry), 0);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Right, GDK_MOD1_MASK));
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (entry)), ==, 3);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_Right, GDK_MOD1_MASK | GDK_SHIFT_MASK));
assert_entry_selection (entry, 3, 7);
gtk_editable_set_position (GTK_EDITABLE (entry), 13);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_BackSpace, GDK_MOD1_MASK));
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "one two ");
gtk_entry_set_text (GTK_ENTRY (entry), "猫 abc def");
gtk_editable_set_position (GTK_EDITABLE (entry), 6);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_BackSpace, GDK_META_MASK));
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "def");
gtk_editable_select_region (GTK_EDITABLE (entry), 0, 2);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_BackSpace, GDK_META_MASK));
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "f");
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE);
gtk_editable_set_position (GTK_EDITABLE (entry), -1);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_BackSpace, GDK_MOD1_MASK));
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_BackSpace, GDK_META_MASK));
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "f");
gtk_widget_destroy (window);
}
static void assert_buffer_text (GtkTextBuffer *buffer, const char *expected)
{
GtkTextIter start, end;
char *text;
gtk_text_buffer_get_bounds (buffer, &start, &end);
text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
g_assert_cmpstr (text, ==, expected);
g_free (text);
}
static void test_macos_multiline_editing (void)
{
GtkWidget *view = gtk_text_view_new ();
GtkWidget *window = shortcut_window (view);
GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
GtkTextIter start, end;
gtk_text_buffer_set_text (buffer, "first\nsecond", -1);
gtk_text_buffer_get_iter_at_offset (buffer, &start, 9);
gtk_text_buffer_place_cursor (buffer, &start);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_Left, GDK_META_MASK));
gtk_text_buffer_get_iter_at_mark (buffer, &start, gtk_text_buffer_get_insert (buffer));
g_assert_cmpint (gtk_text_iter_get_offset (&start), ==, 6);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_Right, GDK_META_MASK | GDK_SHIFT_MASK));
g_assert_true (gtk_text_buffer_get_selection_bounds (buffer, &start, &end));
g_assert_cmpint (gtk_text_iter_get_offset (&start), ==, 6);
g_assert_cmpint (gtk_text_iter_get_offset (&end), ==, 12);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_a, GDK_META_MASK));
g_assert_true (gtk_text_buffer_get_selection_bounds (buffer, &start, &end));
g_assert_cmpint (gtk_text_iter_get_offset (&start), ==, 0);
g_assert_cmpint (gtk_text_iter_get_offset (&end), ==, 12);
gtk_text_buffer_get_iter_at_offset (buffer, &start, 9);
gtk_text_buffer_place_cursor (buffer, &start);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_BackSpace, GDK_META_MASK));
assert_buffer_text (buffer, "first\nond");
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_Down, GDK_META_MASK));
gtk_text_buffer_get_iter_at_mark (buffer, &start, gtk_text_buffer_get_insert (buffer));
g_assert_cmpint (gtk_text_iter_get_offset (&start), ==, 9);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_Up, GDK_META_MASK | GDK_SHIFT_MASK));
g_assert_true (gtk_text_buffer_get_selection_bounds (buffer, &start, &end));
g_assert_cmpint (gtk_text_iter_get_offset (&start), ==, 0);
g_assert_cmpint (gtk_text_iter_get_offset (&end), ==, 9);
/* An existing selection is deleted without removing the rest of its line. */
gtk_text_buffer_get_iter_at_offset (buffer, &start, 7);
gtk_text_buffer_get_iter_at_offset (buffer, &end, 8);
gtk_text_buffer_select_range (buffer, &start, &end);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_BackSpace, GDK_META_MASK));
assert_buffer_text (buffer, "first\nod");
gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
gtk_text_buffer_get_end_iter (buffer, &end);
gtk_text_buffer_place_cursor (buffer, &end);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_BackSpace, GDK_META_MASK));
assert_buffer_text (buffer, "first\nod");
gtk_text_view_set_editable (GTK_TEXT_VIEW (view), TRUE);
{
GtkTextTag *protected = gtk_text_buffer_create_tag (buffer, "protected", "editable", FALSE, NULL);
gtk_text_buffer_get_iter_at_offset (buffer, &start, 6);
gtk_text_buffer_get_iter_at_offset (buffer, &end, 7);
gtk_text_buffer_apply_tag (buffer, protected, &start, &end);
gtk_text_buffer_get_end_iter (buffer, &end);
gtk_text_buffer_place_cursor (buffer, &end);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_BackSpace, GDK_META_MASK));
assert_buffer_text (buffer, "first\no");
}
gtk_text_buffer_set_text (buffer, "one two\nthree four", -1);
gtk_text_buffer_get_end_iter (buffer, &end);
gtk_text_buffer_place_cursor (buffer, &end);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_BackSpace, GDK_MOD1_MASK));
assert_buffer_text (buffer, "one two\nthree ");
gtk_text_buffer_get_iter_at_offset (buffer, &start, 4);
gtk_text_buffer_get_iter_at_offset (buffer, &end, 7);
gtk_text_buffer_select_range (buffer, &start, &end);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_BackSpace, GDK_MOD1_MASK));
assert_buffer_text (buffer, "one \nthree ");
gtk_widget_destroy (window);
}
static guint preferences_opened;
static void test_open_preferences (void)
{
preferences_opened++;
}
static void count_minimize (GtkWindow *window, guint *count)
{
(*count)++;
/* Check action routing without requiring an X11 window manager. */
g_signal_stop_emission_by_name (window, "zoitechat-macos-minimize");
}
static void test_macos_window_shortcuts (void)
{
GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
guint minimized = 0;
preferences_opened = 0;
g_signal_connect (window, "zoitechat-macos-minimize", G_CALLBACK (count_minimize), &minimized);
g_assert_true (gtk_bindings_activate (G_OBJECT (window), GDK_KEY_comma, GDK_META_MASK));
g_assert_cmpuint (preferences_opened, ==, 1);
g_assert_true (gtk_bindings_activate (G_OBJECT (window), GDK_KEY_m, GDK_META_MASK));
g_assert_cmpuint (minimized, ==, 1);
gtk_widget_destroy (window);
}
static void test_macos_legacy_defaults (void)
{
const struct {
guint key, new_key;
GdkModifierType mod, new_mod;
const char *action, *data1, *data2;
gboolean remapped;
} cases[] = {
{GDK_KEY_Tab, GDK_KEY_Tab, GDK_META_MASK, GDK_CONTROL_MASK, "Complete nick/command", "Up", NULL, TRUE},
{GDK_KEY_Left, GDK_KEY_Left, GDK_MOD1_MASK, GDK_META_MASK | GDK_MOD1_MASK, "Move front tab left", NULL, NULL, TRUE},
{GDK_KEY_Right, GDK_KEY_Right, GDK_MOD1_MASK, GDK_META_MASK | GDK_MOD1_MASK, "Move front tab right", "", "", TRUE},
{GDK_KEY_m, GDK_KEY_R, GDK_META_MASK, GDK_META_MASK | GDK_SHIFT_MASK, "Menu Shortcut", "reset-marker", NULL, TRUE},
{GDK_KEY_m, GDK_KEY_m, GDK_META_MASK, GDK_META_MASK, "Menu Shortcut", "search-text", NULL, FALSE},
{GDK_KEY_m, GDK_KEY_m, GDK_CONTROL_MASK, GDK_CONTROL_MASK, "Menu Shortcut", "reset-marker", NULL, FALSE},
{GDK_KEY_Left, GDK_KEY_Left, GDK_MOD1_MASK, GDK_MOD1_MASK, "Move front tab left", "custom", NULL, FALSE},
{GDK_KEY_Tab, GDK_KEY_Tab, GDK_META_MASK, GDK_META_MASK, "Complete nick/command", "Up", "custom", FALSE},
};
guint i;
for (i = 0; i < G_N_ELEMENTS (cases); i++)
{
guint key = cases[i].key;
GdkModifierType mod = cases[i].mod;
g_assert_cmpint (macos_shortcuts_remap_default (&key, &mod, cases[i].action,
cases[i].data1, cases[i].data2), ==, cases[i].remapped);
g_assert_cmpuint (key, ==, cases[i].new_key);
g_assert_cmpuint (mod, ==, cases[i].new_mod);
/* Saved migrated shortcuts must be stable on subsequent launches. */
g_assert_false (macos_shortcuts_remap_default (&key, &mod, cases[i].action,
cases[i].data1, cases[i].data2));
}
}
static void test_search (Fixture *f, gconstpointer data)
{
GError *error = NULL;
@@ -568,6 +896,15 @@ int main (int argc, char **argv)
return 77;
}
prefs.hex_gui_mouse_scroll_speed = 10;
/* Exercise the macOS implementation with real GTK widgets on every OS.
* Production only installs these bindings on macOS. */
macos_shortcuts_init (test_open_preferences);
g_test_add_func ("/macos-shortcuts/entry", test_macos_entry_shortcuts);
g_test_add_func ("/macos-shortcuts/textview", test_macos_textview_shortcuts);
g_test_add_func ("/macos-shortcuts/entry-editing", test_macos_entry_editing);
g_test_add_func ("/macos-shortcuts/multiline-editing", test_macos_multiline_editing);
g_test_add_func ("/macos-shortcuts/window", test_macos_window_shortcuts);
g_test_add_func ("/macos-shortcuts/legacy-defaults", test_macos_legacy_defaults);
#define ADD(name, function) g_test_add ("/xtext/" name, Fixture, NULL, fixture_setup, function, fixture_teardown)
ADD ("append/empty-bounded", test_empty_and_bounded_append);
ADD ("append/utf8-limits", test_utf8_limits);
@@ -577,6 +914,7 @@ int main (int argc, char **argv)
ADD ("layout/wide-metrics", test_wide_metrics);
ADD ("render/formatting-timestamps", test_formatting);
ADD ("selection/clipboard", test_selection_and_clipboard);
ADD ("selection/macos-command-copy", test_macos_chat_copy);
ADD ("search/navigation-regex-clear", test_search);
ADD ("search/follow", test_follow_search);
ADD ("search/unicode-offsets", test_search_unicode_offsets);