Compare commits

...
15 Commits
Author SHA1 Message Date
deepend 6bae96668a add autoaway 2026-10-20 23:47:29 -06:00
deepend-tildeclub 9b65e5be73 Merge pull request #432 from ZoiteChat/fix-macos-keyboard-commands
macos: native editing shortcuts+resolve keybinding conflicts
2026-10-19 15:34:32 -06:00
deepend 9547dd3711 prioritize editing shortcuts 2026-10-19 15:25:28 -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
41 changed files with 1974 additions and 32 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"
+4 -4
View File
@@ -17,7 +17,7 @@ jobs:
artifact-metadata: write artifact-metadata: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v7
with: with:
submodules: true submodules: true
@@ -29,7 +29,7 @@ jobs:
build-essential pkg-config meson ninja-build cmake \ build-essential pkg-config meson ninja-build cmake \
gettext \ gettext \
libcanberra-dev libglib2.0-dev \ libcanberra-dev libglib2.0-dev \
libarchive-dev \ libarchive-dev libxss-dev \
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \ libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
libwayland-client0 libwayland-cursor0 libwayland-egl1 \ libwayland-client0 libwayland-cursor0 libwayland-egl1 \
libxkbcommon0 \ libxkbcommon0 \
@@ -57,7 +57,7 @@ jobs:
ninja -C build ninja -C build
- name: Test emoji catalog, font coverage and picker - name: Test emoji catalog, font coverage and picker
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests" run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Autoaway Tests" "Idle Backend Tests" "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
- name: Install to AppDir - name: Install to AppDir
run: | run: |
@@ -312,7 +312,7 @@ jobs:
subject-path: Zoitechat-*-x86_64.AppImage subject-path: Zoitechat-*-x86_64.AppImage
- name: Upload AppImage artifact - name: Upload AppImage artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: zoitechat-appimage name: zoitechat-appimage
path: Zoitechat-*-x86_64.AppImage path: Zoitechat-*-x86_64.AppImage
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v7
with: with:
submodules: true submodules: true
@@ -39,7 +39,7 @@ jobs:
- name: Upload Flatpak Bundle - name: Upload Flatpak Bundle
id: upload_flatpak id: upload_flatpak
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: zoitechat.flatpak name: zoitechat.flatpak
path: zoitechat.flatpak path: zoitechat.flatpak
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
env: env:
HOMEBREW_NO_AUTO_UPDATE: '1' HOMEBREW_NO_AUTO_UPDATE: '1'
steps: steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install native dependencies - name: Install native dependencies
run: brew bundle --file packaging/macos/Brewfile run: brew bundle --file packaging/macos/Brewfile
- name: Test packaging logic - name: Test packaging logic
@@ -112,7 +112,7 @@ jobs:
env: env:
HOMEBREW_NO_AUTO_UPDATE: '1' HOMEBREW_NO_AUTO_UPDATE: '1'
steps: steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Validate release settings before building - name: Validate release settings before building
+10 -6
View File
@@ -26,9 +26,9 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v7
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: '3.14.3' python-version: '3.14.3'
architecture: ${{ matrix.arch }} architecture: ${{ matrix.arch }}
@@ -138,7 +138,7 @@ jobs:
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
} }
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: '3.8.10' python-version: '3.8.10'
architecture: ${{ matrix.arch }} architecture: ${{ matrix.arch }}
@@ -198,7 +198,7 @@ jobs:
) )
shell: cmd shell: cmd
- name: Test emoji catalog, font coverage and picker - name: Test autoaway, emoji catalog, font coverage and picker
shell: cmd shell: cmd
run: | run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -213,6 +213,10 @@ jobs:
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap ..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
) )
msbuild win32\autoaway-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64
if errorlevel 1 exit /b 1
..\zoitechat-build\x64\tests\autoaway-tests.exe --tap
if errorlevel 1 exit /b 1
- name: Preparing Artifacts - name: Preparing Artifacts
run: | run: |
@@ -222,7 +226,7 @@ jobs:
- name: Upload Installer - name: Upload Installer
id: upload_installer id: upload_installer
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: Installer ${{ matrix.arch }} name: Installer ${{ matrix.arch }}
path: ZoiteChat-*.exe path: ZoiteChat-*.exe
@@ -236,7 +240,7 @@ jobs:
- name: Upload Build Files - name: Upload Build Files
id: upload_buildfiles id: upload_buildfiles
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: Build Files ${{ matrix.arch }} name: Build Files ${{ matrix.arch }}
path: zoitechat-build path: zoitechat-build
+10 -6
View File
@@ -27,9 +27,9 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v7
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: '3.14.3' python-version: '3.14.3'
architecture: ${{ matrix.arch }} architecture: ${{ matrix.arch }}
@@ -146,7 +146,7 @@ jobs:
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
} }
- uses: actions/setup-python@v6 - uses: actions/setup-python@v7
with: with:
python-version: '3.8.10' python-version: '3.8.10'
architecture: ${{ matrix.arch }} architecture: ${{ matrix.arch }}
@@ -209,7 +209,7 @@ jobs:
# Master added native emoji font staging and tests after the release # Master added native emoji font staging and tests after the release
# branch split. Run these before packaging so MSI/MSIX use verified files. # branch split. Run these before packaging so MSI/MSIX use verified files.
- name: Test emoji catalog, font coverage and picker - name: Test autoaway, emoji catalog, font coverage and picker
shell: cmd shell: cmd
run: | run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -224,6 +224,10 @@ jobs:
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap ..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
) )
msbuild win32\autoaway-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64
if errorlevel 1 exit /b 1
..\zoitechat-build\x64\tests\autoaway-tests.exe --tap
if errorlevel 1 exit /b 1
- name: Install WiX Toolset - name: Install WiX Toolset
shell: pwsh shell: pwsh
@@ -399,7 +403,7 @@ jobs:
- name: Upload MSI Installer - name: Upload MSI Installer
id: upload_msi id: upload_msi
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: MSI Installer ${{ matrix.arch }} name: MSI Installer ${{ matrix.arch }}
path: ${{ steps.build_msi.outputs.path }} path: ${{ steps.build_msi.outputs.path }}
@@ -414,7 +418,7 @@ jobs:
- name: Upload MSIX Package - name: Upload MSIX Package
id: upload_msix id: upload_msix
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ steps.build_msix.outputs.artifact_name }} name: ${{ steps.build_msix.outputs.artifact_name }}
path: ${{ steps.build_msix.outputs.path }} path: ${{ steps.build_msix.outputs.path }}
+35
View File
@@ -628,3 +628,38 @@ List of Settings
+------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| **url\_logging** | Toggle logging URLs to *<config>/url.log*. | | **url\_logging** | Toggle logging URLs to *<config>/url.log*. |
+------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Autoaway
--------
In **Preferences > Chatting > General > Away**, set **Autoaway after** to a
number of minutes. The default is 0 (disabled). The separate **Autoaway
message** defaults to "Idle". No plugin or scripting language is required.
The same options are available immediately through ``/SET``::
/SET away_auto_timeout 10
/SET away_auto_reason Idle
/SET away_auto_system ON
With desktop activity enabled, Windows uses the current login session's last
input time, macOS uses CoreGraphics input time, and native X11 uses the
XScreenSaver extension if its development library was available at build time.
Debian/Ubuntu builders can install ``libxss-dev``; Fedora builders can install
``libXScrnSaver-devel``. Meson detects this optional dependency automatically.
On native Wayland, XWayland, unsupported desktops, and builds without XScreenSaver,
autoaway falls back to keyboard, mouse, scroll and touch activity **inside
ZoiteChat**. Activity in other applications does not reset that fallback timer.
Set ``away_auto_system OFF`` to always use ZoiteChat activity on every platform.
The text frontend uses submitted user input. Network traffic, redraws and window
focus changes do not count as user activity.
Autoaway checks every five seconds, after IRC login finishes, independently on
each connected network. Returning clears only away states set by autoaway.
Manual ``/AWAY`` (including ``/AWAY`` while already automatically away) takes
ownership, so activity does not clear it. ``/BACK`` resets the inactivity timer.
The existing **Automatically unmark away** setting keeps its separate behaviour
when sending messages. Do not run another autoaway script at the same time.
Disabling autoaway clears its own away states. Reconnecting re-evaluates current
inactivity instead of restoring an outdated automatic away state; manual away
restoration is unchanged.
+1 -1
View File
@@ -55,7 +55,7 @@
"run-tests": true, "run-tests": true,
"test-rule": "", "test-rule": "",
"test-commands": [ "test-commands": [
"meson test --print-errorlogs 'Emoji Data Tests' 'Emoji Font Tests'" "meson test --print-errorlogs 'Autoaway Tests' 'Idle Backend Tests' 'Emoji Data Tests' 'Emoji Font Tests'"
], ],
"config-opts": [ "config-opts": [
"-Ddbus-service-use-appid=true", "-Ddbus-service-use-appid=true",
+1
View File
@@ -25,6 +25,7 @@ BuildRequires: pkgconfig(gmodule-2.0)
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22 BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(fontconfig) >= 2.13 BuildRequires: pkgconfig(fontconfig) >= 2.13
BuildRequires: pkgconfig(pangoft2) >= 1.44 BuildRequires: pkgconfig(pangoft2) >= 1.44
BuildRequires: pkgconfig(xscrnsaver)
BuildRequires: pkgconfig(iso-codes) BuildRequires: pkgconfig(iso-codes)
BuildRequires: pkgconfig(libarchive) BuildRequires: pkgconfig(libarchive)
BuildRequires: pkgconfig(libcanberra) >= 0.22 BuildRequires: pkgconfig(libcanberra) >= 0.22
+46
View File
@@ -49,6 +49,52 @@ sources and build them in a controlled prefix, or maintain pinned Homebrew formu
## 2. Verify portability and behavior ## 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/X/V in multiline editors use plain UTF-8 text directly, avoiding
GTK3 Quartz's incomplete rich-text negotiation and pasteboard types without a
preferred MIME name. Text, newlines, selection replacement and editability
checks are preserved; rich clipboard formatting is not imported by this action.
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 ```sh
python3 -m unittest discover -s packaging/macos/tests -v python3 -m unittest discover -s packaging/macos/tests -v
python3 packaging/macos/bundle.py audit dist/ZoiteChat.app python3 packaging/macos/bundle.py audit dist/ZoiteChat.app
+135
View File
@@ -0,0 +1,135 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "zoitechat.h"
#include "zoitechatc.h"
#include "server.h"
#include "fe.h"
#include "autoaway.h"
static gint64 last_activity;
static int timer_tag;
static int previous_timeout;
static unsigned int previous_system;
void
autoaway_activity (void)
{
last_activity = g_get_monotonic_time ();
}
/* A manual /AWAY takes ownership even if the server already says away. */
void
autoaway_manual (server *serv)
{
serv->autoaway_owned = FALSE;
autoaway_activity ();
}
void
autoaway_reset (server *serv)
{
if (!serv->autoaway_owned)
return;
serv->autoaway_owned = FALSE;
serv->reconnect_away = FALSE;
g_clear_pointer (&serv->last_away_reason, g_free);
}
static void
autoaway_release (server *serv)
{
if (!serv->autoaway_owned)
return;
/* Cancel even an unacknowledged AWAY. IRC replies may arrive later. */
if (serv->connected && serv->end_of_motd)
serv->p_set_back (serv);
autoaway_reset (serv);
}
void
autoaway_check (gint64 now, gint64 desktop_idle)
{
GSList *list;
gint64 idle = MAX ((gint64)0, now - last_activity);
gint64 limit = (gint64)prefs.hex_away_auto_timeout * 60 * G_USEC_PER_SEC;
if (prefs.hex_away_auto_system && desktop_idle >= 0)
idle = MIN (idle, desktop_idle);
for (list = serv_list; list; list = list->next)
{
server *serv = list->data;
if (!serv->connected)
{
/* Re-evaluate idle after login instead of restoring stale autoaway. */
autoaway_reset (serv);
continue;
}
if (!serv->end_of_motd)
continue;
if (limit <= 0 || idle < limit)
{
autoaway_release (serv);
continue;
}
if (serv->is_away || serv->reconnect_away)
continue;
/* Retry a rejected/lost request slowly, never once per poll. */
if (serv->autoaway_owned &&
now - serv->autoaway_sent_at < 30 * G_USEC_PER_SEC)
continue;
g_free (serv->last_away_reason);
serv->last_away_reason = g_strdup (prefs.hex_away_auto_reason[0] ?
prefs.hex_away_auto_reason : _("Idle"));
serv->autoaway_owned = TRUE;
serv->autoaway_sent_at = now;
serv->p_set_away (serv, serv->last_away_reason);
}
}
static int
autoaway_timer (void)
{
autoaway_check (g_get_monotonic_time (),
prefs.hex_away_auto_system ? fe_get_idle_time () : -1);
return 1;
}
void
autoaway_reinit (void)
{
if (previous_timeout != prefs.hex_away_auto_timeout ||
previous_system != prefs.hex_away_auto_system)
{
autoaway_activity ();
previous_timeout = prefs.hex_away_auto_timeout;
previous_system = prefs.hex_away_auto_system;
}
if (prefs.hex_away_auto_timeout > 0)
{
if (!timer_tag)
{
autoaway_activity ();
timer_tag = fe_timeout_add_seconds (5, autoaway_timer, NULL);
}
}
else
{
if (timer_tag)
{
fe_timeout_remove (timer_tag);
timer_tag = 0;
}
/* Disabling autoaway must also work without another input event. */
autoaway_check (g_get_monotonic_time (), -1);
}
}
+19
View File
@@ -0,0 +1,19 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef ZOITECHAT_AUTOAWAY_H
#define ZOITECHAT_AUTOAWAY_H
#include <glib.h>
struct server;
void autoaway_activity (void);
void autoaway_manual (struct server *serv);
void autoaway_reset (struct server *serv);
void autoaway_reinit (void);
/* Both samples are in microseconds; -1 means desktop idle is unavailable. */
void autoaway_check (gint64 now, gint64 desktop_idle);
#endif
+6
View File
@@ -29,6 +29,7 @@
#include "fe.h" #include "fe.h"
#include "text.h" #include "text.h"
#include "zoitechatc.h" #include "zoitechatc.h"
#include "autoaway.h"
#include "typedef.h" #include "typedef.h"
#ifdef WIN32 #ifdef WIN32
@@ -359,6 +360,9 @@ default_file (void)
const struct prefs vars[] = const struct prefs vars[] =
{ {
{"away_auto_reason", P_OFFSET (hex_away_auto_reason), TYPE_STR},
{"away_auto_system", P_OFFINT (hex_away_auto_system), TYPE_BOOL, autoaway_reinit},
{"away_auto_timeout", P_OFFINT (hex_away_auto_timeout), TYPE_INT, autoaway_reinit},
{"away_auto_unmark", P_OFFINT (hex_away_auto_unmark), TYPE_BOOL}, {"away_auto_unmark", P_OFFINT (hex_away_auto_unmark), TYPE_BOOL},
{"away_omit_alerts", P_OFFINT (hex_away_omit_alerts), TYPE_BOOL}, {"away_omit_alerts", P_OFFINT (hex_away_omit_alerts), TYPE_BOOL},
{"away_reason", P_OFFSET (hex_away_reason), TYPE_STR}, {"away_reason", P_OFFSET (hex_away_reason), TYPE_STR},
@@ -767,6 +771,7 @@ load_default_config(void)
/* put in default values, anything left out is automatically zero */ /* put in default values, anything left out is automatically zero */
/* BOOLEANS */ /* BOOLEANS */
prefs.hex_away_auto_system = 1;
prefs.hex_away_show_once = 1; prefs.hex_away_show_once = 1;
prefs.hex_away_track = 1; prefs.hex_away_track = 1;
prefs.hex_dcc_auto_resume = 1; prefs.hex_dcc_auto_resume = 1;
@@ -887,6 +892,7 @@ load_default_config(void)
prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */ prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */
/* STRINGS */ /* STRINGS */
strcpy (prefs.hex_away_auto_reason, _("Idle"));
strcpy (prefs.hex_away_reason, _("I'm busy")); strcpy (prefs.hex_away_reason, _("I'm busy"));
strcpy (prefs.hex_completion_suffix, ","); strcpy (prefs.hex_completion_suffix, ",");
#ifdef WIN32 #ifdef WIN32
+2
View File
@@ -11,6 +11,7 @@
</ProjectConfiguration> </ProjectConfiguration>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="autoaway.h" />
<ClInclude Include="cfgfiles.h" /> <ClInclude Include="cfgfiles.h" />
<ClInclude Include="chanopt.h" /> <ClInclude Include="chanopt.h" />
<ClInclude Include="ctcp.h" /> <ClInclude Include="ctcp.h" />
@@ -52,6 +53,7 @@
<ClInclude Include="gtk3-theme-service.h" /> <ClInclude Include="gtk3-theme-service.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="autoaway.c" />
<ClCompile Include="cfgfiles.c" /> <ClCompile Include="cfgfiles.c" />
<ClCompile Include="chanopt.c" /> <ClCompile Include="chanopt.c" />
<ClCompile Include="ctcp.c" /> <ClCompile Include="ctcp.c" />
+6
View File
@@ -17,6 +17,9 @@
</Filter> </Filter>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="autoaway.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="cfgfiles.h"> <ClInclude Include="cfgfiles.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -136,6 +139,9 @@
</ClInclude> </ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="autoaway.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="cfgfiles.c"> <ClCompile Include="cfgfiles.c">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
+1
View File
@@ -142,6 +142,7 @@ void fe_lastlog (session *sess, session *lastlog_sess, char *sstr, gtk_xtext_sea
void fe_set_lag (server *serv, long lag); void fe_set_lag (server *serv, long lag);
void fe_set_throttle (server *serv); void fe_set_throttle (server *serv);
void fe_set_away (server *serv); void fe_set_away (server *serv);
gint64 fe_get_idle_time (void);
void fe_serverlist_open (session *sess); void fe_serverlist_open (session *sess);
void fe_get_bool (char *title, char *prompt, void *callback, void *userdata); void fe_get_bool (char *title, char *prompt, void *callback, void *userdata);
void fe_get_str (char *prompt, char *def, void *callback, void *ud); void fe_get_str (char *prompt, char *def, void *callback, void *ud);
+8
View File
@@ -1,4 +1,5 @@
common_sources = [ common_sources = [
'autoaway.c',
'cfgfiles.c', 'cfgfiles.c',
'chanopt.c', 'chanopt.c',
'ctcp.c', 'ctcp.c',
@@ -224,3 +225,10 @@ test('Hex Color Tests', hexcolor_tests,
protocol: 'tap', protocol: 'tap',
timeout: 120, timeout: 120,
) )
autoaway_tests = executable('autoaway_tests',
['tests/test-autoaway.c', 'autoaway.c'],
include_directories: [config_h_include, include_directories('.')],
dependencies: zoitechat_common_header_deps,
)
test('Autoaway Tests', autoaway_tests, protocol: 'tap', timeout: 30)
+12 -1
View File
@@ -54,6 +54,7 @@
#include "server.h" #include "server.h"
#include "tree.h" #include "tree.h"
#include "outbound.h" #include "outbound.h"
#include "autoaway.h"
#include "chanopt.h" #include "chanopt.h"
#define TBUFSIZE 4096 #define TBUFSIZE 4096
@@ -360,6 +361,10 @@ cmd_away (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{ {
char *reason = word_eol[2]; char *reason = word_eol[2];
/* Restoring a manual away after reconnect is not new user input. */
if (!sess->server->reconnect_away)
autoaway_manual (sess->server);
if (!(*reason)) if (!(*reason))
{ {
if (sess->server->is_away) if (sess->server->is_away)
@@ -396,7 +401,10 @@ cmd_away (struct session *sess, char *tbuf, char *word[], char *word_eol[])
static int static int
cmd_back (struct session *sess, char *tbuf, char *word[], char *word_eol[]) cmd_back (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{ {
if (sess->server->is_away) gboolean was_autoaway = sess->server->autoaway_owned;
autoaway_manual (sess->server);
if (sess->server->is_away || was_autoaway)
{ {
sess->server->p_set_back (sess->server); sess->server->p_set_back (sess->server);
} }
@@ -5051,6 +5059,9 @@ handle_user_input (session *sess, char *text, int history, int nocommand)
if (*text == '\0') if (*text == '\0')
return 1; return 1;
if (history)
autoaway_activity ();
if (history) if (history)
history_add (&sess->history, text); history_add (&sess->history, text);
+7
View File
@@ -30,6 +30,7 @@
#include "zoitechat.h" #include "zoitechat.h"
#include "proto-irc.h" #include "proto-irc.h"
#include "autoaway.h"
#include "ctcp.h" #include "ctcp.h"
#include "fe.h" #include "fe.h"
#include "ignore.h" #include "ignore.h"
@@ -438,6 +439,12 @@ irc_raw (server *serv, char *raw)
{ {
int len; int len;
char tbuf[8704]; char tbuf[8704];
/* /QUOTE AWAY and scripts sending raw AWAY also take manual ownership. */
if (!g_ascii_strncasecmp (raw, "AWAY", 4) &&
(raw[4] == 0 || raw[4] == ' '))
autoaway_manual (serv);
if (*raw) if (*raw)
{ {
len = strlen (raw); len = strlen (raw);
+2
View File
@@ -54,6 +54,7 @@
#include "proto-irc.h" #include "proto-irc.h"
#include "servlist.h" #include "servlist.h"
#include "server.h" #include "server.h"
#include "autoaway.h"
#include "sts.h" #include "sts.h"
#ifdef USE_OPENSSL #ifdef USE_OPENSSL
@@ -1873,6 +1874,7 @@ is_server (server *serv)
void void
server_set_defaults (server *serv) server_set_defaults (server *serv)
{ {
autoaway_reset (serv);
g_free (serv->chantypes); g_free (serv->chantypes);
g_clear_pointer (&serv->clienttagdeny, g_free); g_clear_pointer (&serv->clienttagdeny, g_free);
g_free (serv->chanmodes); g_free (serv->chanmodes);
+296
View File
@@ -0,0 +1,296 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <string.h>
#include "../zoitechat.h"
#include "../autoaway.h"
struct zoitechatprefs prefs;
GSList *serv_list;
static server servers[3];
static int away_count, back_count, timer_adds, timer_removes;
static gint64 base;
static char *sent_reason;
/* Protocol stubs deliberately do not acknowledge requests. */
static void
send_away (server *serv, char *reason)
{
away_count++;
g_free (sent_reason);
sent_reason = g_strdup (reason);
}
static void
send_back (server *serv)
{
back_count++;
}
int
fe_timeout_add_seconds (int interval, void *callback, void *data)
{
g_assert_cmpint (interval, ==, 5);
timer_adds++;
return 42;
}
void
fe_timeout_remove (int tag)
{
g_assert_cmpint (tag, ==, 42);
timer_removes++;
}
gint64
fe_get_idle_time (void)
{
return -1;
}
static void
setup (void)
{
int i;
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
for (i = 0; i < 3; i++)
g_free (servers[i].last_away_reason);
g_slist_free (serv_list);
serv_list = NULL;
memset (servers, 0, sizeof servers);
memset (&prefs, 0, sizeof prefs);
away_count = back_count = timer_adds = timer_removes = 0;
g_clear_pointer (&sent_reason, g_free);
prefs.hex_away_auto_timeout = 10;
prefs.hex_away_auto_system = TRUE;
g_strlcpy (prefs.hex_away_auto_reason, "Auto idle", sizeof prefs.hex_away_auto_reason);
for (i = 0; i < 3; i++)
{
servers[i].connected = TRUE;
servers[i].end_of_motd = TRUE;
servers[i].p_set_away = send_away;
servers[i].p_set_back = send_back;
}
serv_list = g_slist_append (NULL, &servers[0]);
autoaway_activity ();
base = g_get_monotonic_time ();
}
static void
test_fallback_boundary (void)
{
setup ();
autoaway_check (base + 599 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 0);
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
g_assert_true (servers[0].autoaway_owned);
g_assert_cmpstr (sent_reason, ==, "Auto idle");
/* An ACK is not required to avoid flooding the connection. */
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[0].is_away = TRUE;
autoaway_check (base + 640 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
g_assert_false (servers[0].autoaway_owned);
g_assert_null (servers[0].last_away_reason);
}
static void
test_desktop_activity (void)
{
setup ();
autoaway_check (base + 900 * G_USEC_PER_SEC, 5 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 0);
autoaway_check (base + 900 * G_USEC_PER_SEC, 700 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 1);
servers[0].is_away = TRUE;
autoaway_check (base + 905 * G_USEC_PER_SEC, 0);
g_assert_cmpint (back_count, ==, 1);
/* App input wins even if the native backend still has an old sample. */
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), 900 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 1);
}
static void
test_app_mode (void)
{
setup ();
prefs.hex_away_auto_system = FALSE;
autoaway_check (base + 600 * G_USEC_PER_SEC, 0);
g_assert_cmpint (away_count, ==, 1);
}
static void
test_manual_away (void)
{
setup ();
servers[0].is_away = TRUE;
servers[0].last_away_reason = g_strdup ("Lunch");
autoaway_check (base + 900 * G_USEC_PER_SEC, -1);
autoaway_check (base + 905 * G_USEC_PER_SEC, 0);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (away_count, ==, 0);
g_assert_cmpint (back_count, ==, 0);
g_assert_cmpstr (servers[0].last_away_reason, ==, "Lunch");
}
static void
test_manual_takeover (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
servers[0].is_away = TRUE;
/* This is also called for /AWAY with no reason, even if already away. */
autoaway_manual (&servers[0]);
autoaway_check (g_get_monotonic_time (), 0);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 0);
g_assert_true (servers[0].is_away);
}
static void
test_pending_away_cancel (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_false (servers[0].is_away);
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
/* A late 306 must not make us own the away state again. */
servers[0].is_away = TRUE;
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
}
static void
test_login_and_multiple_servers (void)
{
setup ();
serv_list = g_slist_append (serv_list, &servers[1]);
serv_list = g_slist_append (serv_list, &servers[2]);
servers[1].is_away = TRUE;
servers[1].last_away_reason = g_strdup ("Manual");
servers[2].end_of_motd = FALSE;
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[2].end_of_motd = TRUE;
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
autoaway_check (base + 610 * G_USEC_PER_SEC, 0);
g_assert_cmpint (back_count, ==, 2);
g_assert_cmpstr (servers[1].last_away_reason, ==, "Manual");
}
static void
test_disconnect_and_reconnect (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
servers[0].connected = FALSE;
servers[0].is_away = TRUE;
servers[0].reconnect_away = TRUE;
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_false (servers[0].autoaway_owned);
g_assert_false (servers[0].reconnect_away);
g_assert_null (servers[0].last_away_reason);
g_assert_cmpint (back_count, ==, 0);
servers[0].is_away = FALSE;
servers[0].connected = TRUE;
servers[0].end_of_motd = FALSE;
autoaway_check (base + 610 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[0].end_of_motd = TRUE;
autoaway_check (base + 615 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
/* server_set_defaults resets ownership even before the next timer tick. */
servers[0].reconnect_away = TRUE;
autoaway_reset (&servers[0]);
g_assert_false (servers[0].reconnect_away);
/* Existing manual reconnect-away must be left alone. */
servers[0].reconnect_away = TRUE;
servers[0].last_away_reason = g_strdup ("Manual");
autoaway_reset (&servers[0]);
g_assert_true (servers[0].reconnect_away);
g_assert_cmpstr (servers[0].last_away_reason, ==, "Manual");
}
static void
test_retry (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
autoaway_check (base + 629 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
autoaway_check (base + 630 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
}
static void
test_disable_and_timers (void)
{
setup ();
autoaway_reinit ();
autoaway_reinit ();
g_assert_cmpint (timer_adds, ==, 1);
base = g_get_monotonic_time ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 1);
g_assert_cmpint (timer_removes, ==, 1);
g_assert_false (servers[0].autoaway_owned);
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 1);
prefs.hex_away_auto_timeout = -1;
autoaway_reinit ();
g_assert_cmpint (away_count, ==, 1);
}
static void
test_empty_reason_and_large_timeout (void)
{
setup ();
prefs.hex_away_auto_reason[0] = 0;
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_nonnull (sent_reason);
g_assert_cmpstr (sent_reason, !=, "");
setup ();
prefs.hex_away_auto_timeout = G_MAXINT;
autoaway_check (base + 900 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 0);
}
int
main (int argc, char **argv)
{
int result, i;
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/autoaway/fallback-boundary", test_fallback_boundary);
g_test_add_func ("/autoaway/desktop-activity", test_desktop_activity);
g_test_add_func ("/autoaway/app-mode", test_app_mode);
g_test_add_func ("/autoaway/manual-away", test_manual_away);
g_test_add_func ("/autoaway/manual-takeover", test_manual_takeover);
g_test_add_func ("/autoaway/pending-cancel", test_pending_away_cancel);
g_test_add_func ("/autoaway/login-multiple-servers", test_login_and_multiple_servers);
g_test_add_func ("/autoaway/reconnect", test_disconnect_and_reconnect);
g_test_add_func ("/autoaway/retry", test_retry);
g_test_add_func ("/autoaway/disable-timers", test_disable_and_timers);
g_test_add_func ("/autoaway/empty-reason-large-timeout", test_empty_reason_and_large_timeout);
result = g_test_run ();
for (i = 0; i < 3; i++)
g_free (servers[i].last_away_reason);
g_free (sent_reason);
g_slist_free (serv_list);
return result;
}
+3
View File
@@ -46,6 +46,7 @@
#include "plugin.h" #include "plugin.h"
#include "plugin-identd.h" #include "plugin-identd.h"
#include "plugin-timer.h" #include "plugin-timer.h"
#include "autoaway.h"
#include "notify.h" #include "notify.h"
#include "server.h" #include "server.h"
#include "servlist.h" #include "servlist.h"
@@ -499,6 +500,8 @@ zoitechat_reinit_timers (void)
static int lag_check_tag = 0; static int lag_check_tag = 0;
static int away_tag = 0; static int away_tag = 0;
autoaway_reinit ();
if (prefs.hex_notify_timeout && notify_tag == 0) if (prefs.hex_notify_timeout && notify_tag == 0)
{ {
notify_tag = fe_timeout_add_seconds (prefs.hex_notify_timeout, notify_tag = fe_timeout_add_seconds (prefs.hex_notify_timeout,
+5
View File
@@ -105,6 +105,7 @@ struct zoitechatprefs
/* these are the rebranded, consistent, sorted zoitechat variables */ /* these are the rebranded, consistent, sorted zoitechat variables */
/* BOOLEANS */ /* BOOLEANS */
unsigned int hex_away_auto_system;
unsigned int hex_away_auto_unmark; unsigned int hex_away_auto_unmark;
unsigned int hex_away_omit_alerts; unsigned int hex_away_omit_alerts;
unsigned int hex_away_show_once; unsigned int hex_away_show_once;
@@ -233,6 +234,7 @@ struct zoitechatprefs
unsigned int hex_url_logging; unsigned int hex_url_logging;
/* NUMBERS */ /* NUMBERS */
int hex_away_auto_timeout;
int hex_away_size_max; int hex_away_size_max;
int hex_away_timeout; int hex_away_timeout;
int hex_completion_amount; int hex_completion_amount;
@@ -311,6 +313,7 @@ struct zoitechatprefs
int hex_url_grabber_limit; int hex_url_grabber_limit;
/* STRINGS */ /* STRINGS */
char hex_away_auto_reason[256];
char hex_away_reason[256]; char hex_away_reason[256];
char hex_completion_suffix[4]; /* Only ever holds a one-character string. */ char hex_completion_suffix[4]; /* Only ever holds a one-character string. */
char hex_dcc_completed_dir[PATHLEN + 1]; char hex_dcc_completed_dir[PATHLEN + 1];
@@ -597,6 +600,7 @@ typedef struct server
/*time_t connect_time;*/ /* when did it connect? */ /*time_t connect_time;*/ /* when did it connect? */
unsigned long lag_sent; /* we are still waiting for this ping response*/ unsigned long lag_sent; /* we are still waiting for this ping response*/
time_t ping_recv; /* when we last got a ping reply */ time_t ping_recv; /* when we last got a ping reply */
gint64 autoaway_sent_at;
time_t away_time; /* when we were marked away */ time_t away_time; /* when we were marked away */
char *encoding; char *encoding;
@@ -616,6 +620,7 @@ typedef struct server
unsigned int end_of_motd:1; /* end of motd reached (logged in) */ unsigned int end_of_motd:1; /* end of motd reached (logged in) */
unsigned int sent_quit:1; /* sent a QUIT already? */ unsigned int sent_quit:1; /* sent a QUIT already? */
unsigned int use_listargs:1; /* undernet and dalnet need /list >0,<10000 */ unsigned int use_listargs:1; /* undernet and dalnet need /list >0,<10000 */
unsigned int autoaway_owned:1;
unsigned int is_away:1; unsigned int is_away:1;
unsigned int reconnect_away:1; /* whether to reconnect in is_away state */ unsigned int reconnect_away:1; /* whether to reconnect in is_away state */
unsigned int dont_use_proxy:1; /* to proxy or not to proxy */ unsigned int dont_use_proxy:1; /* to proxy or not to proxy */
+5
View File
@@ -23,6 +23,7 @@
#include "fe-gtk.h" #include "fe-gtk.h"
#ifdef __APPLE__ #ifdef __APPLE__
#include "../common/macos.h" #include "../common/macos.h"
#include "macos-shortcuts.h"
#endif #endif
#ifdef GDK_WINDOWING_WIN32 #ifdef GDK_WINDOWING_WIN32
@@ -58,6 +59,7 @@
#include "notifygui.h" #include "notifygui.h"
#include "textgui.h" #include "textgui.h"
#include "fkeys.h" #include "fkeys.h"
#include "idle.h"
#include "plugin-tray.h" #include "plugin-tray.h"
#include "urlgrab.h" #include "urlgrab.h"
#include "setup.h" #include "setup.h"
@@ -503,6 +505,8 @@ fe_args (int argc, char *argv[])
gtk_init (&argc, &argv); gtk_init (&argc, &argv);
#ifdef __APPLE__ #ifdef __APPLE__
macos_shortcuts_init (setup_open);
if (g_getenv ("ZOITECHAT_MACOS_SHARE")) if (g_getenv ("ZOITECHAT_MACOS_SHARE"))
{ {
char *icons = g_build_filename (g_getenv ("ZOITECHAT_MACOS_SHARE"), "icons", NULL); char *icons = g_build_filename (g_getenv ("ZOITECHAT_MACOS_SHARE"), "icons", NULL);
@@ -635,6 +639,7 @@ fe_init (void)
{ {
theme_manager_init (); theme_manager_init ();
key_init (); key_init ();
idle_init ();
pixmaps_init (); pixmaps_init ();
#ifdef HAVE_GTK_MAC #ifdef HAVE_GTK_MAC
+3 -1
View File
@@ -31,7 +31,7 @@
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;user32.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol> <EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
@@ -49,6 +49,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
</PreBuildEvent> </PreBuildEvent>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="idle.h" />
<ClInclude Include="ascii.h" /> <ClInclude Include="ascii.h" />
<ClInclude Include="banlist.h" /> <ClInclude Include="banlist.h" />
<ClInclude Include="chanlist.h" /> <ClInclude Include="chanlist.h" />
@@ -91,6 +92,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="theme\theme-preferences.h" /> <ClInclude Include="theme\theme-preferences.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="idle.c" />
<ClCompile Include="ascii.c" /> <ClCompile Include="ascii.c" />
<ClCompile Include="banlist.c" /> <ClCompile Include="banlist.c" />
<ClCompile Include="chanlist.c" /> <ClCompile Include="chanlist.c" />
+6
View File
@@ -15,6 +15,9 @@
</Filter> </Filter>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="idle.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ascii.h"> <ClInclude Include="ascii.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -137,6 +140,9 @@
</ClInclude> </ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="idle.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ascii.c"> <ClCompile Include="ascii.c">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
+63
View File
@@ -55,6 +55,9 @@
#include "maingui.h" #include "maingui.h"
#include "textgui.h" #include "textgui.h"
#include "fkeys.h" #include "fkeys.h"
#ifdef __APPLE__
#include "macos-shortcuts.h"
#endif
#define ICON_FKEYS_NEW "document-new" #define ICON_FKEYS_NEW "document-new"
#define ICON_FKEYS_DELETE "edit-delete" #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); 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) switch (evt->keyval)
{ {
case GDK_KEY_space: case GDK_KEY_space:
@@ -1333,6 +1342,57 @@ key_save_kbs (void)
#define KBSTATE_DT1 3 #define KBSTATE_DT1 3
#define KBSTATE_DT2 4 #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 \ #define STRIP_WHITESPACE \
while (buf[0] == ' ' || buf[0] == '\t') \ while (buf[0] == ' ' || buf[0] == '\t') \
buf++; \ buf++; \
@@ -1519,6 +1579,9 @@ key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list)
} }
} }
g_free (ibuf); g_free (ibuf);
#ifdef __APPLE__
key_macos_adjust_defaults (list);
#endif
*out_list = list; *out_list = list;
return 0; return 0;
+3
View File
@@ -36,5 +36,8 @@ int key_action_insert (GtkWidget * wid, GdkEventKey * evt, char *d1, char *d2,
session *sess); session *sess);
void key_check_replace_on_change (GtkEditable *editable, gpointer data); void key_check_replace_on_change (GtkEditable *editable, gpointer data);
gboolean key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod); gboolean key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod);
#ifdef __APPLE__
gboolean key_is_bound (guint keyval, GdkModifierType mod);
#endif
#endif #endif
+98
View File
@@ -0,0 +1,98 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "config.h"
#include <gtk/gtk.h>
#include "../common/autoaway.h"
#include "idle.h"
#ifdef WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
#include <math.h>
#elif defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
#include <gdk/gdkx.h>
#include <X11/extensions/scrnsaver.h>
#endif
/* Do not count focus changes, redraws, timers or incoming IRC traffic. */
static void
idle_event (GdkEvent *event, gpointer data)
{
switch (event->type)
{
case GDK_KEY_PRESS:
case GDK_BUTTON_PRESS:
case GDK_SCROLL:
case GDK_MOTION_NOTIFY:
case GDK_TOUCH_BEGIN:
case GDK_TOUCH_UPDATE:
autoaway_activity ();
break;
default:
break;
}
gtk_main_do_event (event);
}
void
idle_init (void)
{
autoaway_activity ();
/* Keep GTK's normal dispatch, including detached windows and dialogs. */
gdk_event_handler_set (idle_event, NULL, NULL);
}
/* Microseconds since desktop input, or -1 for the portable app fallback. */
gint64
fe_get_idle_time (void)
{
#ifdef WIN32
LASTINPUTINFO input = {0};
DWORD elapsed;
input.cbSize = sizeof input;
if (!GetLastInputInfo (&input))
return -1;
/* DWORD subtraction handles the 49-day GetTickCount wrap. Some injected
* input has a slightly newer timestamp; do not treat it as weeks idle. */
elapsed = GetTickCount () - input.dwTime;
if ((gint32)elapsed < 0)
elapsed = 0;
return (gint64)elapsed * 1000;
#elif defined(__APPLE__)
CFTimeInterval seconds = CGEventSourceSecondsSinceLastEventType (
kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType);
if (!isfinite (seconds) || seconds < 0 ||
seconds >= (double)G_MAXINT64 / G_USEC_PER_SEC)
return -1;
return (gint64)(seconds * G_USEC_PER_SEC);
#elif defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
GdkDisplay *display = gdk_display_get_default ();
XScreenSaverInfo info = {0};
Display *xdisplay;
int event_base, error_base, error;
Status ok;
const char *wayland = g_getenv ("WAYLAND_DISPLAY");
/* XWayland idle is not desktop-wide. Never pass a Wayland display to Xlib. */
if (!display || !GDK_IS_X11_DISPLAY (display) || (wayland && *wayland))
return -1;
xdisplay = gdk_x11_display_get_xdisplay (display);
gdk_x11_display_error_trap_push (display);
ok = XScreenSaverQueryExtension (xdisplay, &event_base, &error_base);
if (ok)
ok = XScreenSaverQueryInfo (xdisplay, DefaultRootWindow (xdisplay), &info);
error = gdk_x11_display_error_trap_pop (display);
if (!ok || error)
return -1;
return (gint64)info.idle * 1000;
#else
return -1;
#endif
}
+10
View File
@@ -0,0 +1,10 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef ZOITECHAT_IDLE_H
#define ZOITECHAT_IDLE_H
void idle_init (void);
#endif
+391
View File
@@ -0,0 +1,391 @@
/* 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
copy_text (GtkTextView *view)
{
GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
GtkTextIter start, end;
if (gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
{
gchar *text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
gtk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (view),
GDK_SELECTION_CLIPBOARD), text, -1);
g_free (text);
}
}
static void
cut_text (GtkTextView *view)
{
copy_text (view);
gtk_text_buffer_delete_selection (gtk_text_view_get_buffer (view), TRUE,
gtk_text_view_get_editable (view));
gtk_text_view_reset_im_context (view);
gtk_text_view_scroll_mark_onscreen (view,
gtk_text_buffer_get_insert (gtk_text_view_get_buffer (view)));
}
typedef struct
{
GtkTextView *view;
GtkTextBuffer *buffer;
} TextPaste;
static void
text_paste_received (GtkClipboard *clipboard, const gchar *text, gpointer userdata)
{
TextPaste *paste = userdata;
GtkTextView *view = paste->view;
GtkTextBuffer *buffer = paste->buffer;
/* Clipboard replies may arrive after a view was closed or switched to
* another buffer. Retain the request's objects, but never edit a new one. */
if (text && gtk_widget_get_realized (GTK_WIDGET (view)) &&
gtk_text_view_get_buffer (view) == buffer)
{
gboolean editable = gtk_text_view_get_editable (view);
gtk_text_buffer_begin_user_action (buffer);
gtk_text_buffer_delete_selection (buffer, TRUE, editable);
gtk_text_buffer_insert_interactive_at_cursor (buffer, text, -1, 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));
g_signal_emit_by_name (buffer, "paste-done", clipboard);
}
g_object_unref (buffer);
g_object_unref (view);
g_free (paste);
}
static void
paste_text (GtkTextView *view)
{
TextPaste *paste = g_new (TextPaste, 1);
paste->view = g_object_ref (view);
paste->buffer = g_object_ref (gtk_text_view_get_buffer (view));
/* ZoiteChat's editors consume text. GTK3's Quartz rich-text fallback
* enumerates pasteboard UTIs without MIME names (a NULL atom assertion)
* and its rich-text request callback is unimplemented. Request UTF-8
* directly, retaining interactive selection and read-only behavior. */
gtk_clipboard_request_text (gtk_widget_get_clipboard (GTK_WIDGET (view),
GDK_SELECTION_CLIPBOARD), text_paste_received, paste);
}
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);
}
static void
install_text_bindings (GtkSettings *settings, GParamSpec *property,
GtkCssProvider *provider)
{
GtkWidget *widgets[2];
const char *selectors[] = {"entry", "textview"};
const char *names[] = {"zoitechat-macos-entry", "zoitechat-macos-textview"};
GdkScreen *screen = gdk_screen_get_default ();
GString *css = g_string_new (NULL);
guint i, j;
/* CSS key themes take precedence over class bindings. Put our small
* sets first, retaining the theme's remaining bindings as fallbacks.
* Remove our previous provider before reading a changed key theme. */
g_object_ref (provider);
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (provider));
widgets[0] = gtk_entry_new ();
widgets[1] = gtk_text_view_new ();
for (i = 0; i < G_N_ELEMENTS (widgets); i++)
{
GPtrArray *fallbacks = NULL;
GtkStyleContext *context = gtk_widget_get_style_context (widgets[i]);
g_object_ref_sink (widgets[i]);
gtk_style_context_get (context, gtk_style_context_get_state (context),
"-gtk-key-bindings", &fallbacks, NULL);
g_string_append_printf (css, "%s { -gtk-key-bindings: %s", selectors[i], names[i]);
for (j = 0; fallbacks && j < fallbacks->len; j++)
{
GtkBindingSet *binding = g_ptr_array_index (fallbacks, j);
g_string_append_printf (css, ", %s", binding->set_name);
}
g_string_append (css, "; }\n");
if (fallbacks)
g_ptr_array_unref (fallbacks);
gtk_widget_destroy (widgets[i]);
g_object_unref (widgets[i]);
}
gtk_css_provider_load_from_data (provider, css->str, css->len, NULL);
gtk_style_context_add_provider_for_screen (screen, GTK_STYLE_PROVIDER (provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_string_free (css, TRUE);
g_object_unref (provider);
}
void
macos_shortcuts_init (void (*callback) (void))
{
GType types[] = {GTK_TYPE_ENTRY, GTK_TYPE_TEXT_VIEW};
const char *names[] = {"zoitechat-macos-entry", "zoitechat-macos-textview"};
GtkCssProvider *provider;
gpointer window_class;
GtkBindingSet *window_bindings;
static gboolean initialized = FALSE;
guint i;
preferences_callback = callback;
if (initialized)
return;
initialized = TRUE;
/* 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_new (names[i]);
gtk_binding_entry_skip (bindings, GDK_KEY_c, GDK_CONTROL_MASK);
gtk_binding_entry_skip (bindings, GDK_KEY_x, GDK_CONTROL_MASK);
gtk_binding_entry_skip (bindings, GDK_KEY_v, GDK_CONTROL_MASK);
if (types[i] == GTK_TYPE_TEXT_VIEW)
{
g_signal_new_class_handler ("zoitechat-macos-copy-text", types[i],
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (copy_text), NULL, NULL, NULL,
G_TYPE_NONE, 0);
g_signal_new_class_handler ("zoitechat-macos-cut-text", types[i],
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (cut_text), NULL, NULL, NULL,
G_TYPE_NONE, 0);
g_signal_new_class_handler ("zoitechat-macos-paste-text", types[i],
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_CALLBACK (paste_text), NULL, NULL, NULL,
G_TYPE_NONE, 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_v, GDK_META_MASK,
"zoitechat-macos-paste-text", 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_c, GDK_META_MASK,
"zoitechat-macos-copy-text", 0);
gtk_binding_entry_add_signal (bindings, GDK_KEY_x, GDK_META_MASK,
"zoitechat-macos-cut-text", 0);
}
else
{
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);
}
provider = gtk_css_provider_new ();
install_text_bindings (gtk_settings_get_default (), NULL, provider);
g_signal_connect_object (gtk_settings_get_default (), "notify::gtk-key-theme-name",
G_CALLBACK (install_text_bindings), provider, 0);
g_signal_connect_object (gtk_settings_get_default (), "notify::gtk-theme-name",
G_CALLBACK (install_text_bindings), provider, 0);
g_object_unref (provider);
/* 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 "chanlist.h"
#include "editlist.h" #include "editlist.h"
#include "fkeys.h" #include "fkeys.h"
#ifdef __APPLE__
#include "macos-shortcuts.h"
#endif
#include "gtkutil.h" #include "gtkutil.h"
#include "maingui.h" #include "maingui.h"
#include "notifygui.h" #include "notifygui.h"
@@ -1322,6 +1325,17 @@ menu_settings (GtkWidget * wid, gpointer none)
setup_open (); 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 static void
menu_usermenu (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_("_Copy Selection"), menu_copy_selection, 0, M_MENUITEM, 0, 0, 1},
{N_("C_lear Text"), menu_flushbuffer, 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}, {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"), 0, 0, M_MENUSUB, 0, 0, 1},
{N_("Search Text" ELLIPSIS), menu_search, 0, M_MENUITEM, 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}, {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"; return "fullscreen-toggle";
case AWAY_OFFSET: case AWAY_OFFSET:
return "away-toggle"; return "away-toggle";
case 65:
return "reset-marker";
case 66: case 66:
return "move-marker"; return "reset-marker";
case 67: case 67:
return "move-marker";
case 68:
return "copy-selection"; return "copy-selection";
case SEARCH_OFFSET + 1: case SEARCH_OFFSET + 1:
return "search-text"; return "search-text";
@@ -2118,7 +2132,7 @@ menu_get_key_action_name (int index)
return "search-next"; return "search-next";
case SEARCH_OFFSET + 3: case SEARCH_OFFSET + 3:
return "search-previous"; return "search-previous";
case 75: case 77:
return "contents"; 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)); 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 static void
menu_refresh_keybinding_accels (GtkWidget *widget, gpointer data) 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"); 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); menu_add_keybinding_accel (widget, accel_group, name);
submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget)); 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); menu_close (NULL, NULL);
else if (!strcmp (name, "quit")) 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; return FALSE;
menu_quit (NULL, NULL); menu_quit (NULL, NULL);
} }
@@ -2841,6 +2880,16 @@ menu_create_main (void *accel_group, int bar, int away, int toplevel,
#endif #endif
gtk_menu_shell_append (GTK_MENU_SHELL (menu_bar), menu_item); gtk_menu_shell_append (GTK_MENU_SHELL (menu_bar), menu_item);
gtk_widget_show (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; break;
case M_MENUPIX: case M_MENUPIX:
@@ -2854,6 +2903,10 @@ menu_create_main (void *accel_group, int bar, int away, int toplevel,
case M_MENUITEM: case M_MENUITEM:
item = gtk_menu_item_new_with_mnemonic (_(mymenu[i].text)); item = gtk_menu_item_new_with_mnemonic (_(mymenu[i].text));
normalitem: 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)); 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)); 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)) if (mymenu[i].key != 0 && !(mymenu[i].id == MENU_ID_QUIT && !prefs.hex_gui_ctrlq_quit))
+24 -2
View File
@@ -25,6 +25,7 @@ zoitechat_gtk_sources = [
'gtkutil.c', 'gtkutil.c',
'icon-resolver.c', 'icon-resolver.c',
'ignoregui.c', 'ignoregui.c',
'idle.c',
'inline-image.c', 'inline-image.c',
'joind.c', 'joind.c',
'menu.c', 'menu.c',
@@ -45,6 +46,7 @@ zoitechat_gtk_sources = [
] ]
zoitechat_gtk_cflags = [] zoitechat_gtk_cflags = []
idle_deps = []
zoitechat_gtk_deps = [ zoitechat_gtk_deps = [
zoitechat_common_dep, zoitechat_common_dep,
@@ -93,9 +95,17 @@ if host_machine.system() != 'windows'
zoitechat_gtk_deps += gdk_x11_dep zoitechat_gtk_deps += gdk_x11_dep
endif endif
xss_dep = dependency('xscrnsaver', required: false)
if gdk_x11_dep.found() and xss_dep.found()
zoitechat_gtk_deps += xss_dep
idle_deps += [gdk_x11_dep, xss_dep]
zoitechat_gtk_cflags += '-DHAVE_XSS'
endif
x11_dep = dependency('x11', required: false) x11_dep = dependency('x11', required: false)
if x11_dep.found() if x11_dep.found()
zoitechat_gtk_deps += x11_dep zoitechat_gtk_deps += x11_dep
idle_deps += x11_dep
endif endif
endif endif
@@ -105,12 +115,16 @@ zoitechat_gtk_objcflags = []
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
zoitechat_gtk_sources += 'notifications/notification-windows.c' zoitechat_gtk_sources += 'notifications/notification-windows.c'
zoitechat_gtk_deps += cc.find_library('dwmapi', required: true) zoitechat_gtk_deps += cc.find_library('dwmapi', required: true)
idle_deps += cc.find_library('user32', required: true)
zoitechat_gtk_deps += idle_deps
zoitechat_theme_deps += cc.find_library('dwmapi', required: true) zoitechat_theme_deps += cc.find_library('dwmapi', required: true)
elif host_machine.system() == 'darwin' elif host_machine.system() == 'darwin'
add_languages('objc', required: true) add_languages('objc', required: true)
zoitechat_gtk_sources += 'macos-shortcuts.c'
zoitechat_gtk_sources += 'notifications/notification-osx.m' zoitechat_gtk_sources += 'notifications/notification-osx.m'
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications']) zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications', 'CoreGraphics'])
idle_deps += dependency('appleframeworks', modules: ['CoreGraphics'])
zoitechat_gtk_objcflags += '-fobjc-arc' zoitechat_gtk_objcflags += '-fobjc-arc'
else else
zoitechat_gtk_sources += 'notifications/notification-freedesktop.c' zoitechat_gtk_sources += 'notifications/notification-freedesktop.c'
@@ -296,8 +310,16 @@ test('Emoji Font Tests', emoji_font_tests,
) )
xtext_tests = executable('xtext_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], include_directories: [config_h_include],
dependencies: [gtk_dep], dependencies: [gtk_dep],
) )
test('XText Tests', xtext_tests, protocol: 'tap', timeout: 120) test('XText Tests', xtext_tests, protocol: 'tap', timeout: 120)
idle_tests = executable('idle_tests',
'tests/test-idle.c',
include_directories: [config_h_include],
dependencies: [gtk_dep] + idle_deps,
c_args: zoitechat_gtk_cflags,
)
test('Idle Backend Tests', idle_tests, protocol: 'tap', timeout: 30)
+8
View File
@@ -558,6 +558,10 @@ static const setting general_settings[] =
{ST_ENTRY, N_("Away:"), P_OFFSETNL(hex_away_reason), 0, 0, sizeof prefs.hex_away_reason}, {ST_ENTRY, N_("Away:"), P_OFFSETNL(hex_away_reason), 0, 0, sizeof prefs.hex_away_reason},
{ST_HEADER, N_("Away"),0,0,0}, {ST_HEADER, N_("Away"),0,0,0},
{ST_NUMBER, N_("Autoaway after:"), P_OFFINTNL(hex_away_auto_timeout), N_("Set to 0 to disable automatic away."), (const char **)N_("minutes (0 = off)."), 1440},
{ST_ENTRY, N_("Autoaway message:"), P_OFFSETNL(hex_away_auto_reason), 0, 0, sizeof prefs.hex_away_auto_reason},
{ST_TOGGLE, N_("Use desktop activity when available"), P_OFFINTNL(hex_away_auto_system), N_("Use desktop input on Windows, macOS and X11. Otherwise, use keyboard and mouse activity in ZoiteChat."), 0, 0},
{ST_LABEL, N_("On Wayland, autoaway uses activity in ZoiteChat.\nOnly automatically set away states are cleared when activity resumes.")},
{ST_TOGGLE, N_("Show away once"), P_OFFINTNL(hex_away_show_once), N_("Show identical away messages only once."), 0, 0}, {ST_TOGGLE, N_("Show away once"), P_OFFINTNL(hex_away_show_once), N_("Show identical away messages only once."), 0, 0},
{ST_TOGGLE, N_("Automatically unmark away"), P_OFFINTNL(hex_away_auto_unmark), N_("Unmark yourself as away before sending messages."), 0, 0}, {ST_TOGGLE, N_("Automatically unmark away"), P_OFFINTNL(hex_away_auto_unmark), N_("Unmark yourself as away before sending messages."), 0, 0},
@@ -567,7 +571,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 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 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}, {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}, {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} {ST_END, 0, 0, 0, 0, 0}
}; };
+1
View File
@@ -23,5 +23,6 @@
#include "theme/theme-manager.h" #include "theme/theme-manager.h"
void setup_apply_real (const ThemeChangedEvent *event); void setup_apply_real (const ThemeChangedEvent *event);
void setup_open (void);
#endif #endif
+85
View File
@@ -0,0 +1,85 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <gtk/gtk.h>
#include "../idle.h"
static int activity_count, dispatch_count;
static void test_gtk_dispatch (GdkEvent *event);
void
autoaway_activity (void)
{
activity_count++;
}
/* Exercise the real handler without delivering synthetic input to a widget. */
#define gtk_main_do_event test_gtk_dispatch
#include "../idle.c"
#undef gtk_main_do_event
static void
test_gtk_dispatch (GdkEvent *event)
{
dispatch_count++;
}
static void
test_user_input_and_dispatch (void)
{
GdkEvent event = {0};
GdkEventType input[] = {GDK_KEY_PRESS, GDK_BUTTON_PRESS, GDK_SCROLL,
GDK_MOTION_NOTIFY, GDK_TOUCH_BEGIN, GDK_TOUCH_UPDATE};
GdkEventType other[] = {GDK_FOCUS_CHANGE, GDK_CONFIGURE, GDK_WINDOW_STATE,
GDK_PROPERTY_NOTIFY, GDK_VISIBILITY_NOTIFY};
unsigned int i;
activity_count = dispatch_count = 0;
for (i = 0; i < G_N_ELEMENTS (input); i++)
{
event.type = input[i];
idle_event (&event, NULL);
}
g_assert_cmpint (activity_count, ==, G_N_ELEMENTS (input));
for (i = 0; i < G_N_ELEMENTS (other); i++)
{
event.type = other[i];
idle_event (&event, NULL);
}
g_assert_cmpint (activity_count, ==, G_N_ELEMENTS (input));
g_assert_cmpint (dispatch_count, ==, G_N_ELEMENTS (input) + G_N_ELEMENTS (other));
}
static void
test_native_idle (void)
{
gint64 idle = fe_get_idle_time ();
g_assert_cmpint (idle, >=, -1);
#if defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
if (gdk_display_get_default () && GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
{
/* XWayland must use the app fallback, even if XSS is compiled in. */
char *saved = g_strdup (g_getenv ("WAYLAND_DISPLAY"));
g_unsetenv ("WAYLAND_DISPLAY");
g_assert_cmpint (fe_get_idle_time (), >=, 0);
g_setenv ("WAYLAND_DISPLAY", "wayland-test", TRUE);
g_assert_cmpint (fe_get_idle_time (), ==, -1);
if (saved)
g_setenv ("WAYLAND_DISPLAY", saved, TRUE);
else
g_unsetenv ("WAYLAND_DISPLAY");
g_free (saved);
}
#endif
}
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
gtk_init_check (&argc, &argv);
g_test_add_func ("/idle/user-input-and-dispatch", test_user_input_and_dispatch);
g_test_add_func ("/idle/native-backend", test_native_idle);
return g_test_run ();
}
+503
View File
@@ -3,6 +3,7 @@
* adding test-only symbols to the widget's public API. Application services * adding test-only symbols to the widget's public API. Application services
* are stubbed; GTK, Pango, Cairo and the widget itself are exercised normally. */ * are stubbed; GTK, Pango, Cairo and the widget itself are exercised normally. */
#include "../xtext.c" #include "../xtext.c"
#include "../macos-shortcuts.h"
#include <glib/gstdio.h> #include <glib/gstdio.h>
struct zoitechatprefs prefs; struct zoitechatprefs prefs;
@@ -252,6 +253,470 @@ static void test_selection_and_clipboard (Fixture *f, gconstpointer data)
g_assert_false (gtk_xtext_is_selecting (f->xtext)); 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);
/* Give mapped editor fixtures a stable allocation before the first draw. */
gtk_window_set_default_size (GTK_WINDOW (window), 640, 240);
gtk_container_set_border_width (GTK_CONTAINER (window), 16);
if (GTK_IS_ENTRY (widget))
{
gtk_widget_set_size_request (widget, 320, 40);
gtk_widget_set_valign (widget, GTK_ALIGN_CENTER);
}
gtk_container_add (GTK_CONTAINER (window), widget);
gtk_widget_show_all (window);
drain_events ();
return window;
}
static void test_macos_entry_shortcuts (gconstpointer data)
{
gboolean search = GPOINTER_TO_INT (data);
GtkWidget *entry = g_object_ref_sink (search ? gtk_search_entry_new () : gtk_entry_new ());
GtkWidget *window;
GtkClipboard *clipboard;
gint64 deadline;
if (search)
{
/* Inheritance and clipboard actions do not require drawing. Quartz
* SearchEntry icon rendering can emit an unrelated fatal warning.
* Keep this variant attached to a screen but unmapped; the ordinary
* Entry variant below still exercises a mapped, editable widget. */
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_container_add (GTK_CONTAINER (window), entry);
g_assert_true (gtk_widget_has_screen (entry));
g_assert_false (gtk_widget_get_mapped (entry));
}
else
{
window = shortcut_window (entry);
g_assert_true (gtk_widget_get_mapped (entry));
}
clipboard = gtk_widget_get_clipboard (entry, GDK_SELECTION_CLIPBOARD);
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 猫");
/* Password fields are ordinary entries, not icon-bearing search fields. */
if (!search)
{
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 unexpected_textview_clipboard_action (GtkTextView *view, gpointer data)
{
guint *count = data;
GSignalInvocationHint *hint = g_signal_get_invocation_hint (view);
(*count)++;
/* Fail on incorrect dispatch, before GTK's Quartz rich-text path runs. */
g_signal_stop_emission (view, hint->signal_id, hint->detail);
}
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;
guint default_actions = 0;
g_signal_connect (view, "copy-clipboard", G_CALLBACK (unexpected_textview_clipboard_action), &default_actions);
g_signal_connect (view, "cut-clipboard", G_CALLBACK (unexpected_textview_clipboard_action), &default_actions);
g_signal_connect (view, "paste-clipboard", G_CALLBACK (unexpected_textview_clipboard_action), &default_actions);
gtk_text_buffer_set_text (buffer, "two\nlines", -1);
g_test_message ("Select multiline text");
gtk_text_buffer_get_bounds (buffer, &start, &end);
gtk_text_buffer_select_range (buffer, &start, &end);
g_test_message ("Copy multiline text");
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_c, GDK_META_MASK));
g_assert_cmpuint (default_actions, ==, 0);
assert_clipboard (view, "two\nlines");
g_test_message ("Cut multiline text");
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_x, GDK_META_MASK));
g_assert_cmpuint (default_actions, ==, 0);
g_assert_cmpint (gtk_text_buffer_get_char_count (buffer), ==, 0);
assert_clipboard (view, "two\nlines");
g_test_message ("Paste multiline text");
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_v, GDK_META_MASK));
g_assert_cmpuint (default_actions, ==, 0);
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_key_theme_fallback (void)
{
GtkWidget *entry = gtk_entry_new ();
GtkWidget *window = shortcut_window (entry);
gint start, end;
/* A key-theme reload must keep our overrides first and retain the
* Mac theme's Emacs-style Control+A/E navigation for other keys. */
g_object_notify (G_OBJECT (gtk_settings_get_default ()), "gtk-key-theme-name");
g_object_notify (G_OBJECT (gtk_settings_get_default ()), "gtk-theme-name");
gtk_entry_set_text (GTK_ENTRY (entry), "one two");
gtk_editable_set_position (GTK_EDITABLE (entry), -1);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_a, GDK_CONTROL_MASK));
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (entry)), ==, 0);
g_assert_false (gtk_editable_get_selection_bounds (GTK_EDITABLE (entry), &start, &end));
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_e, GDK_CONTROL_MASK));
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (entry)), ==, 7);
g_assert_true (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_a, GDK_META_MASK));
assert_entry_selection (entry, 0, 7);
g_assert_false (gtk_bindings_activate (G_OBJECT (entry), GDK_KEY_v, GDK_CONTROL_MASK));
gtk_widget_destroy (window);
}
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 paste_completed (GtkTextBuffer *buffer, GtkClipboard *clipboard, guint *count)
{
(*count)++;
}
static void wait_for_paste (guint *count, guint expected)
{
gint64 deadline = g_get_monotonic_time () + 2 * G_TIME_SPAN_SECOND;
while (*count < expected && g_get_monotonic_time () < deadline)
{
drain_events ();
g_usleep (1000);
}
g_assert_cmpuint (*count, ==, expected);
}
static void text_with_native_type (GtkClipboard *clipboard, GtkSelectionData *selection,
guint info, gpointer data)
{
if (info == 0)
gtk_selection_data_set_text (selection, data, -1);
}
static void test_macos_multiline_paste (void)
{
GtkWidget *view = gtk_text_view_new ();
GtkWidget *window = shortcut_window (view);
GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
GtkClipboard *clipboard = gtk_widget_get_clipboard (view, GDK_SELECTION_CLIPBOARD);
GtkTextIter start, end;
guint pasted = 0;
GtkTargetEntry targets[] = {
{(gchar *) "UTF8_STRING", 0, 0},
/* This base UTI has no preferred MIME name on Quartz. A text paste
* must not enumerate/convert it through GTK's rich-text fallback. */
{(gchar *) "public.item", 0, 1}
};
g_signal_connect (buffer, "paste-done", G_CALLBACK (paste_completed), &pasted);
gtk_text_buffer_set_text (buffer, "before after", -1);
gtk_text_buffer_get_iter_at_offset (buffer, &start, 7);
gtk_text_buffer_get_end_iter (buffer, &end);
gtk_text_buffer_select_range (buffer, &start, &end);
g_assert_true (gtk_clipboard_set_with_data (clipboard, targets, G_N_ELEMENTS (targets),
text_with_native_type, NULL, (gpointer) "猫\npasted"));
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_v, GDK_META_MASK));
wait_for_paste (&pasted, 1);
assert_buffer_text (buffer, "before 猫\npasted");
gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
gtk_text_buffer_get_bounds (buffer, &start, &end);
gtk_text_buffer_select_range (buffer, &start, &end);
gtk_clipboard_set_text (clipboard, "blocked", -1);
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_v, GDK_META_MASK));
wait_for_paste (&pasted, 2);
assert_buffer_text (buffer, "before 猫\npasted");
g_assert_false (gtk_text_view_get_editable (GTK_TEXT_VIEW (view)));
g_assert_true (gtk_bindings_activate (G_OBJECT (view), GDK_KEY_x, GDK_META_MASK));
assert_buffer_text (buffer, "before 猫\npasted");
assert_clipboard (view, "before 猫\npasted");
gtk_clipboard_clear (clipboard);
gtk_widget_destroy (window);
}
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) static void test_search (Fixture *f, gconstpointer data)
{ {
GError *error = NULL; GError *error = NULL;
@@ -567,7 +1032,44 @@ int main (int argc, char **argv)
g_test_message ("GTK display unavailable; run under Xvfb for widget coverage"); g_test_message ("GTK display unavailable; run under Xvfb for widget coverage");
return 77; return 77;
} }
g_object_set (gtk_settings_get_default (), "gtk-enable-animations", FALSE, NULL);
/* Quartz installs this CSS key theme automatically. Exercise its higher
* priority bindings on Linux too, rather than only testing GTK classes. */
g_object_set (gtk_settings_get_default (), "gtk-key-theme-name", "Mac", NULL);
if (!gtk_binding_set_find ("gtk-mac-cmd-c"))
{
/* Some Linux packages omit GTK's Mac theme. Reproduce the upstream
* CSS clipboard bindings so this regression is still tested there. */
GtkCssProvider *keys = gtk_css_provider_new ();
g_test_message ("Using portable Mac key-theme fixture");
gtk_css_provider_load_from_data (keys,
"@binding-set zoitechat-test-mac-clipboard {"
" bind '<meta>c' { 'copy-clipboard' () };"
" bind '<meta>x' { 'cut-clipboard' () };"
" bind '<meta>v' { 'paste-clipboard' () };"
" bind '<ctrl>a' { 'move-cursor' (paragraph-ends, -1, 0) };"
" bind '<ctrl>e' { 'move-cursor' (paragraph-ends, 1, 0) }; }"
"entry, textview { -gtk-key-bindings: zoitechat-test-mac-clipboard; }",
-1, NULL);
gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
GTK_STYLE_PROVIDER (keys), GTK_STYLE_PROVIDER_PRIORITY_SETTINGS);
g_object_unref (keys);
}
else
g_test_message ("Using GTK's installed Mac key theme");
prefs.hex_gui_mouse_scroll_speed = 10; 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_data_func ("/macos-shortcuts/entry", GINT_TO_POINTER (FALSE), test_macos_entry_shortcuts);
g_test_add_data_func ("/macos-shortcuts/search-entry", GINT_TO_POINTER (TRUE), test_macos_entry_shortcuts);
g_test_add_func ("/macos-shortcuts/textview", test_macos_textview_shortcuts);
g_test_add_func ("/macos-shortcuts/key-theme-fallback", test_macos_key_theme_fallback);
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/multiline-paste", test_macos_multiline_paste);
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) #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/empty-bounded", test_empty_and_bounded_append);
ADD ("append/utf8-limits", test_utf8_limits); ADD ("append/utf8-limits", test_utf8_limits);
@@ -577,6 +1079,7 @@ int main (int argc, char **argv)
ADD ("layout/wide-metrics", test_wide_metrics); ADD ("layout/wide-metrics", test_wide_metrics);
ADD ("render/formatting-timestamps", test_formatting); ADD ("render/formatting-timestamps", test_formatting);
ADD ("selection/clipboard", test_selection_and_clipboard); 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/navigation-regex-clear", test_search);
ADD ("search/follow", test_follow_search); ADD ("search/follow", test_follow_search);
ADD ("search/unicode-offsets", test_search_unicode_offsets); ADD ("search/unicode-offsets", test_search_unicode_offsets);
+6
View File
@@ -48,6 +48,12 @@
#endif #endif
gint64
fe_get_idle_time (void)
{
return -1;
}
static int done = FALSE; static int done = FALSE;
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{EE388A27-6992-43C0-BA9B-61D8968D42E3}</ProjectGuid>
<SolutionDir>$(MSBuildThisFileDirectory)</SolutionDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="zoitechat.props" />
<PropertyGroup>
<TargetName>autoaway-tests</TargetName>
<OutDir>$(ZoiteChatBuild)\$(ZoiteChatPlatform)\tests\</OutDir>
<IntDir>$(ZoiteChatObj)autoaway-tests\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>WIN32;_WIN64;_CONSOLE;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>gio-2.0.lib;gobject-2.0.lib;glib-2.0.lib;$(IntlLib);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\src\common\autoaway.c" />
<ClCompile Include="..\src\common\tests\test-autoaway.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>