Compare commits

...
Author SHA1 Message Date
deepend 8edd669067 refine more sections 2026-10-25 03:32:06 -06:00
deepend 81f4f591f5 fix favicon 2026-10-24 13:14:22 -06:00
deepend-tildeclub 8e7885c7f6 Merge pull request #434 from ZoiteChat/fix-timestamps
Fix timestamps when toggling nick indentation
2026-10-22 04:29:54 -06:00
deepend 03c4e872c0 Fix timestamps when toggling nick indentation 2026-10-20 15:33: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
53 changed files with 8188 additions and 1976 deletions

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
version: 2
updates:
# Flatpak shared modules
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 2
labels:
- "dependencies"
# GitHub Actions dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 3
labels:
- "dependencies"
commit-message:
prefix: "ci"
cooldown:
default-days: 7
groups:
actions-minor-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
artifact-metadata: write artifact-metadata: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v7
with: with:
submodules: true submodules: true
@@ -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
+5 -5
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 }}
@@ -222,7 +222,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 +236,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
+5 -5
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 }}
@@ -399,7 +399,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 +414,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 }}
+2 -4
View File
@@ -138,10 +138,8 @@ html_context = {
# of the sidebar. # of the sidebar.
html_logo = '_static/zoitechat-logo.svg' html_logo = '_static/zoitechat-logo.svg'
# The name of an image file (within the static path) to use as favicon of the # Use the ZoiteChat logo instead of the old HexChat favicon.
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 html_favicon = '_static/zoitechat-logo.svg'
# pixels large.
html_favicon = '_static/favicon.ico'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
+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
+48 -22
View File
@@ -68,7 +68,7 @@ static ca_context *ca_con;
#define SCROLLBACK_MAX 32000 #define SCROLLBACK_MAX 32000
static void mkdir_p (char *filename); static void mkdir_p (char *filename);
static char *log_create_filename (char *channame); static char *log_create_filename (const char *channame);
static char * static char *
scrollback_get_filename (session *sess) scrollback_get_filename (session *sess)
@@ -396,7 +396,7 @@ mkdir_p (char *filename)
} }
static char * static char *
log_create_filename (char *channame) log_create_filename (const char *channame)
{ {
char *tmp, *ret; char *tmp, *ret;
int mbl; int mbl;
@@ -453,7 +453,7 @@ log_escape_strcpy (char *dest, char *src, char *end)
/* substitutes %c %n %s into buffer */ /* substitutes %c %n %s into buffer */
static void static void
log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s) log_insert_vars (char *buf, int bufsize, const char *fmt, char *c, char *n, char *s)
{ {
char *end = buf + bufsize; char *end = buf + bufsize;
@@ -466,6 +466,14 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
return; return;
case '%': case '%':
/* A partially typed custom format can end in a literal percent. */
if (!fmt[1])
{
buf[0] = '%';
if (buf + 1 < end) buf[1] = 0;
else buf[0] = 0;
return;
}
fmt++; fmt++;
switch (fmt[0]) switch (fmt[0])
{ {
@@ -479,6 +487,11 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
buf = log_escape_strcpy (buf, s, end); buf = log_escape_strcpy (buf, s, end);
break; break;
default: default:
if (buf + 1 == end)
{
buf[0] = 0;
return;
}
buf[0] = '%'; buf[0] = '%';
buf++; buf++;
buf[0] = fmt[0]; buf[0] = fmt[0];
@@ -500,61 +513,74 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
} }
} }
static char * char *
log_create_pathname (char *servname, char *channame, char *netname) log_build_pathname (const char *mask, const char *config_dir,
const char *servname, const char *channame,
const char *netname, time_t now)
{ {
char fname[384]; char fname[384];
char fnametime[384]; char fnametime[384];
time_t now; char *safe_channel, *safe_network, *safe_server;
if (!mask || !*mask || !config_dir || !servname || !channame ||
!g_utf8_validate (mask, -1, NULL))
return NULL;
if (!netname) if (!netname)
{ {
netname = g_strdup ("NETWORK"); safe_network = g_strdup ("NETWORK");
} }
else else
{ {
netname = log_create_filename (netname); safe_network = log_create_filename (netname);
} }
/* first, everything is in UTF-8 */ /* first, everything is in UTF-8 */
if (!rfc_casecmp (channame, servname)) if (!rfc_casecmp (channame, servname))
{ {
channame = g_strdup ("server"); safe_channel = g_strdup ("server");
} }
else else
{ {
channame = log_create_filename (channame); safe_channel = log_create_filename (channame);
} }
servname = log_create_filename (servname); safe_server = log_create_filename (servname);
log_insert_vars (fname, sizeof (fname), prefs.hex_irc_logmask, channame, netname, servname); log_insert_vars (fname, sizeof (fname), mask, safe_channel, safe_network, safe_server);
g_free (channame); g_free (safe_channel);
g_free (netname); g_free (safe_network);
g_free (servname); g_free (safe_server);
/* insert time/date */ /* insert time/date */
now = time (NULL); if (!g_utf8_validate (fname, -1, NULL) ||
strftime_utf8 (fnametime, sizeof (fnametime), fname, now); !strftime_utf8 (fnametime, sizeof (fnametime), fname, now))
return NULL;
/* If one uses log mask variables, such as "%c/...", %c will be empty upon /* If one uses log mask variables, such as "%c/...", %c will be empty upon
* connecting since there's no channel name yet, so we have to make sure * connecting since there's no channel name yet, so we have to make sure
* we won't try to write to the FS root. */ * we won't try to write to the FS root. */
if (g_path_is_absolute (prefs.hex_irc_logmask)) if (g_path_is_absolute (mask))
{ {
g_snprintf (fname, sizeof (fname), "%s", fnametime); g_snprintf (fname, sizeof (fname), "%s", fnametime);
} }
else /* relative path */ else /* relative path */
{ {
g_snprintf (fname, sizeof (fname), "%s" G_DIR_SEPARATOR_S "logs" G_DIR_SEPARATOR_S "%s", get_xdir (), fnametime); g_snprintf (fname, sizeof (fname), "%s" G_DIR_SEPARATOR_S "logs" G_DIR_SEPARATOR_S "%s", config_dir, fnametime);
} }
/* create all the subdirectories */
mkdir_p (fname);
return g_strdup (fname); return g_strdup (fname);
} }
static char *
log_create_pathname (char *servname, char *channame, char *netname)
{
char *filename = log_build_pathname (prefs.hex_irc_logmask, get_xdir (),
servname, channame, netname, time (NULL));
if (filename) mkdir_p (filename);
return filename;
}
static int static int
log_open_file (char *servname, char *channame, char *netname) log_open_file (char *servname, char *channame, char *netname)
{ {
+5
View File
@@ -47,6 +47,11 @@ void PrintTextf (session *sess, const char *format, ...) G_GNUC_PRINTF (2, 3);
void PrintTextTimeStampf (session *sess, time_t timestamp, const char *format, ...) G_GNUC_PRINTF (3, 4); void PrintTextTimeStampf (session *sess, time_t timestamp, const char *format, ...) G_GNUC_PRINTF (3, 4);
void log_close (session *sess); void log_close (session *sess);
void log_open_or_close (session *sess); void log_open_or_close (session *sess);
/* Resolve a log filename without creating directories or changing preferences.
* The caller owns the UTF-8 result; NULL means the format could not be expanded. */
char *log_build_pathname (const char *mask, const char *config_dir,
const char *servname, const char *channame,
const char *netname, time_t now);
void load_text_events (void); void load_text_events (void);
void pevent_save (char *fn); void pevent_save (char *fn);
int pevt_build_string (const char *input, char **output, int *max_arg); int pevt_build_string (const char *input, char **output, int *max_arg);
+300
View File
@@ -41,6 +41,8 @@
#include "gtkutil.h" #include "gtkutil.h"
#include "maingui.h" #include "maingui.h"
#include "editlist.h" #include "editlist.h"
#include "preferences-persistence.h"
#include "setup.h"
#define ICON_EDITLIST_NEW "document-new" #define ICON_EDITLIST_NEW "document-new"
#define ICON_EDITLIST_DELETE "edit-delete" #define ICON_EDITLIST_DELETE "edit-delete"
@@ -373,3 +375,301 @@ editlist_gui_open (char *title1, char *title2, GSList *list, char *title, char *
gtk_widget_show (editlist_win); gtk_widget_show (editlist_win);
} }
/* Preferences editors are independent instances: several lists may be open
* at once, and their models must never share the legacy dialog globals. */
typedef struct
{
GtkWidget *view;
GtkWidget *delete_button;
GtkWidget *up_button;
GtkWidget *down_button;
GSList **target;
const char *file;
char *original;
char *pending;
} PreferencesList;
static GSList *preferences_lists;
static void
preferences_list_update_buttons (PreferencesList *editor)
{
GtkTreeModel *model;
GtkTreeIter iter, next;
GtkTreePath *path;
gboolean selected = gtk_tree_selection_get_selected (
gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), &model, &iter);
gboolean up = FALSE, down = FALSE;
if (selected)
{
path = gtk_tree_model_get_path (model, &iter);
up = gtk_tree_path_get_indices (path)[0] > 0;
gtk_tree_path_free (path);
next = iter;
down = gtk_tree_model_iter_next (model, &next);
}
gtk_widget_set_sensitive (editor->delete_button, selected);
gtk_widget_set_sensitive (editor->up_button, up);
gtk_widget_set_sensitive (editor->down_button, down);
}
static void
preferences_list_selection_changed (GtkTreeSelection *selection, PreferencesList *editor)
{
preferences_list_update_buttons (editor);
}
static void
preferences_list_rows_reordered (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
gint *order, PreferencesList *editor)
{
preferences_list_update_buttons (editor);
}
static char *
preferences_list_serialize (GSList *list)
{
GString *text = g_string_new ("");
for (; list; list = list->next)
{
struct popup *item = list->data;
g_string_append_printf (text, "NAME %s\nCMD %s\n\n", item->name, item->cmd);
}
return g_string_free (text, FALSE);
}
static void
preferences_list_free (gpointer data)
{
PreferencesList *editor = data;
preferences_lists = g_slist_remove (preferences_lists, editor);
g_free (editor->original);
g_free (editor->pending);
g_free (editor);
}
static void
preferences_list_edited (GtkCellRenderer *renderer, char *path, char *text, PreferencesList *editor)
{
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (editor->view));
if (gtk_tree_model_get_iter_from_string (model, &iter, path))
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
GPOINTER_TO_INT (g_object_get_data (G_OBJECT (renderer), "column")), text, -1);
}
static void
preferences_list_add (GtkButton *button, PreferencesList *editor)
{
GtkTreeIter iter;
GtkTreePath *path;
GtkTreeView *view = GTK_TREE_VIEW (editor->view);
GtkListStore *store = GTK_LIST_STORE (gtk_tree_view_get_model (view));
(void)button;
gtk_list_store_insert_with_values (store, &iter, -1, NAME_COLUMN, "", CMD_COLUMN, "", -1);
path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
gtk_tree_view_set_cursor (view, path, gtk_tree_view_get_column (view, 0), TRUE);
gtk_tree_view_scroll_to_cell (view, path, NULL, FALSE, 0, 0);
gtk_tree_path_free (path);
preferences_list_update_buttons (editor);
}
static void
preferences_list_delete (GtkButton *button, PreferencesList *editor)
{
GtkTreeIter iter;
GtkTreeModel *model;
(void)button;
if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), &model, &iter))
gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
}
static void
preferences_list_move (GtkButton *button, PreferencesList *editor)
{
GtkTreeModel *model;
GtkTreeIter iter, other;
GtkTreePath *path;
gboolean valid;
if (!gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), &model, &iter))
return;
path = gtk_tree_model_get_path (model, &iter);
if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "direction")) < 0)
valid = gtk_tree_path_prev (path);
else { gtk_tree_path_next (path); valid = TRUE; }
if (valid && gtk_tree_model_get_iter (model, &other, path))
{
gtk_list_store_swap (GTK_LIST_STORE (model), &iter, &other);
gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (editor->view), path, NULL, FALSE, 0, 0);
}
gtk_tree_path_free (path);
}
static gboolean
preferences_list_key (GtkWidget *view, GdkEventKey *event, PreferencesList *editor)
{
GtkWidget *button;
int direction;
(void)view;
if (!(event->state & GDK_SHIFT_MASK) || (event->keyval != GDK_KEY_Up && event->keyval != GDK_KEY_Down))
return FALSE;
direction = event->keyval == GDK_KEY_Up ? -1 : 1;
button = gtk_button_new ();
g_object_ref_sink (button);
g_object_set_data (G_OBJECT (button), "direction", GINT_TO_POINTER (direction));
preferences_list_move (GTK_BUTTON (button), editor);
g_object_unref (button);
return TRUE;
}
GtkWidget *
editlist_preferences_create (GSList **list, const char *file, const char *title1,
const char *title2, const char *help)
{
PreferencesList *editor = g_new0 (PreferencesList, 1);
GtkWidget *box, *scroll, *buttons, *button;
GtkListStore *store;
GtkCellRenderer *renderer;
int column;
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
editor->target = list;
editor->file = file;
editor->original = preferences_list_serialize (*list);
preferences_lists = g_slist_append (preferences_lists, editor);
g_object_set_data_full (G_OBJECT (box), "preferences-list", editor, preferences_list_free);
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING);
editor->view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (editor->view), TRUE);
gtk_tree_view_set_search_column (GTK_TREE_VIEW (editor->view), NAME_COLUMN);
for (column = 0; column < N_COLUMNS; column++)
{
GtkTreeViewColumn *col;
renderer = gtk_cell_renderer_text_new ();
g_object_set (renderer, "editable", TRUE, NULL);
g_object_set_data (G_OBJECT (renderer), "column", GINT_TO_POINTER (column));
g_signal_connect (renderer, "edited", G_CALLBACK (preferences_list_edited), editor);
col = gtk_tree_view_column_new_with_attributes (column ? title2 : title1, renderer, "text", column, NULL);
gtk_tree_view_column_set_resizable (col, TRUE);
gtk_tree_view_column_set_min_width (col, column ? 240 : 140);
gtk_tree_view_column_set_expand (col, column != 0);
gtk_tree_view_append_column (GTK_TREE_VIEW (editor->view), col);
}
editlist_load (store, *list);
g_object_unref (store);
g_signal_connect (editor->view, "key-press-event", G_CALLBACK (preferences_list_key), editor);
scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 220);
gtk_container_add (GTK_CONTAINER (scroll), editor->view);
gtk_box_pack_start (GTK_BOX (box), scroll, TRUE, TRUE, 0);
buttons = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
gtk_box_pack_start (GTK_BOX (box), buttons, FALSE, FALSE, 0);
button = gtk_button_new_with_mnemonic (_("_Add"));
g_signal_connect (button, "clicked", G_CALLBACK (preferences_list_add), editor);
gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0);
button = gtk_button_new_with_mnemonic (_("_Delete"));
editor->delete_button = button;
g_signal_connect (button, "clicked", G_CALLBACK (preferences_list_delete), editor);
gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0);
for (column = -1; column <= 1; column += 2)
{
button = gtk_button_new_with_label (column < 0 ? _("Move up") : _("Move down"));
if (column < 0) editor->up_button = button;
else editor->down_button = button;
g_object_set_data (G_OBJECT (button), "direction", GINT_TO_POINTER (column));
g_signal_connect (button, "clicked", G_CALLBACK (preferences_list_move), editor);
gtk_box_pack_start (GTK_BOX (buttons), button, FALSE, FALSE, 0);
}
g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (editor->view)), "changed",
G_CALLBACK (preferences_list_selection_changed), editor);
g_signal_connect (gtk_tree_view_get_model (GTK_TREE_VIEW (editor->view)), "rows-reordered",
G_CALLBACK (preferences_list_rows_reordered), editor);
preferences_list_update_buttons (editor);
if (help)
{
GtkWidget *expander = gtk_expander_new (_("Formatting codes and examples"));
GtkWidget *label = gtk_label_new (help);
gtk_label_set_xalign (GTK_LABEL (label), 0);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_max_width_chars (GTK_LABEL (label), 60);
gtk_widget_set_margin_top (label, 10);
gtk_container_add (GTK_CONTAINER (expander), label);
gtk_box_pack_start (GTK_BOX (box), expander, FALSE, FALSE, 0);
}
return box;
}
gboolean
editlist_preferences_stage (GError **error)
{
GSList *entry;
for (entry = preferences_lists; entry; entry = entry->next)
{
PreferencesList *editor = entry->data;
GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (editor->view));
GtkTreeIter iter;
GString *text = g_string_new ("");
gboolean valid = gtk_tree_model_get_iter_first (model, &iter);
char *live;
g_clear_pointer (&editor->pending, g_free);
while (valid)
{
char *name, *cmd;
gtk_tree_model_get (model, &iter, NAME_COLUMN, &name, CMD_COLUMN, &cmd, -1);
if ((name && strpbrk (name, "\r\n")) || (cmd && strpbrk (cmd, "\r\n")) ||
((!name || !*name) && cmd && *cmd))
{
GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
int column = !name || !*name || strpbrk (name, "\r\n") ? NAME_COLUMN : CMD_COLUMN;
setup_reveal_widget (editor->view);
gtk_widget_grab_focus (editor->view);
gtk_tree_view_set_cursor (GTK_TREE_VIEW (editor->view), path,
gtk_tree_view_get_column (GTK_TREE_VIEW (editor->view), column), TRUE);
gtk_tree_path_free (path);
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
_("Give this entry a name and keep each field on one line."));
g_free (name); g_free (cmd); g_string_free (text, TRUE);
return FALSE;
}
if (name && *name)
g_string_append_printf (text, "NAME %s\nCMD %s\n\n", name, cmd ? cmd : "");
g_free (name); g_free (cmd);
valid = gtk_tree_model_iter_next (model, &iter);
}
if (strcmp (text->str, editor->original) == 0) { g_string_free (text, TRUE); continue; }
live = preferences_list_serialize (*editor->target);
if (strcmp (live, editor->original) != 0)
{
setup_reveal_widget (editor->view);
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_AGAIN,
_("These entries changed outside Preferences. Reopen Preferences before editing them."));
g_free (live); g_string_free (text, TRUE);
return FALSE;
}
g_free (live);
editor->pending = g_string_free (text, FALSE);
preferences_persistence_stage_file (editor->file, editor->pending);
}
return TRUE;
}
void
editlist_preferences_commit (void)
{
GSList *entry, *sessions;
for (entry = preferences_lists; entry; entry = entry->next)
{
PreferencesList *editor = entry->data;
if (!editor->pending) continue;
list_free (editor->target);
list_loadconf ((char *)editor->file, editor->target, NULL);
if (editor->target == &usermenu_list) usermenu_update ();
for (sessions = sess_list; sessions; sessions = sessions->next)
{
if (editor->target == &button_list) fe_buttons_update (sessions->data);
if (editor->target == &dlgbutton_list) fe_dlgbuttons_update (sessions->data);
}
}
}
+5
View File
@@ -21,5 +21,10 @@
#define ZOITECHAT_EDITLIST_H #define ZOITECHAT_EDITLIST_H
void editlist_gui_open (char *title1, char *title2, GSList * list, char *title, char *wmclass, char *file, char *help); void editlist_gui_open (char *title1, char *title2, GSList * list, char *title, char *wmclass, char *file, char *help);
GtkWidget *editlist_preferences_create (GSList **list, const char *file,
const char *title1, const char *title2,
const char *help);
gboolean editlist_preferences_stage (GError **error);
void editlist_preferences_commit (void);
#endif #endif
+3
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
@@ -503,6 +504,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);
+2
View File
@@ -63,6 +63,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="joind.h" /> <ClInclude Include="joind.h" />
<ClInclude Include="maingui.h" /> <ClInclude Include="maingui.h" />
<ClInclude Include="preferences-persistence.h" /> <ClInclude Include="preferences-persistence.h" />
<ClInclude Include="preferences-pages.h" />
<ClInclude Include="menu.h" /> <ClInclude Include="menu.h" />
<ClInclude Include="notifications\notification-backend.h" /> <ClInclude Include="notifications\notification-backend.h" />
<ClInclude Include="notifygui.h" /> <ClInclude Include="notifygui.h" />
@@ -113,6 +114,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClCompile Include="notifygui.c" /> <ClCompile Include="notifygui.c" />
<ClCompile Include="pixmaps.c" /> <ClCompile Include="pixmaps.c" />
<ClCompile Include="preferences-persistence.c" /> <ClCompile Include="preferences-persistence.c" />
<ClCompile Include="preferences-pages.c" />
<ClCompile Include="plugin-notification.c" /> <ClCompile Include="plugin-notification.c" />
<ClCompile Include="plugin-tray.c" /> <ClCompile Include="plugin-tray.c" />
<ClCompile Include="plugingui.c" /> <ClCompile Include="plugingui.c" />
+6
View File
@@ -57,6 +57,9 @@
<ClInclude Include="preferences-persistence.h"> <ClInclude Include="preferences-persistence.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="preferences-pages.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="menu.h"> <ClInclude Include="menu.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -194,6 +197,9 @@
<ClCompile Include="preferences-persistence.c"> <ClCompile Include="preferences-persistence.c">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="preferences-pages.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="plugingui.c"> <ClCompile Include="plugingui.c">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
+208 -674
View File
@@ -55,6 +55,12 @@
#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
#include "setup.h"
#include "preferences-persistence.h"
#include "preferences-pages.h"
#define ICON_FKEYS_NEW "document-new" #define ICON_FKEYS_NEW "document-new"
#define ICON_FKEYS_DELETE "edit-delete" #define ICON_FKEYS_DELETE "edit-delete"
@@ -107,7 +113,7 @@ struct gcomp_data
static int key_load_kbs (void); static int key_load_kbs (void);
static int key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list); static int key_load_kbs_from_buffer (char *ibuf, off_t size, GSList **out_list);
static int key_save_kbs (void);
static void key_dialog_load (GtkListStore *store); static void key_dialog_load (GtkListStore *store);
static void key_dialog_reset (GtkWidget *wid, gpointer userdata); static void key_dialog_reset (GtkWidget *wid, gpointer userdata);
static int key_action_handle_command (GtkWidget * wid, GdkEventKey * evt, static int key_action_handle_command (GtkWidget * wid, GdkEventKey * evt,
@@ -156,53 +162,6 @@ static int key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt,
static GSList *keybind_list = NULL; static GSList *keybind_list = NULL;
#define KEY_DIALOG_THEME_LISTENER_ID_KEY "fkeys.theme-listener-id"
static void
key_dialog_theme_apply (GtkWidget *window)
{
GtkWidget *xtext;
XTextColor xtext_palette[XTEXT_COLS];
if (!window)
return;
xtext = g_object_get_data (G_OBJECT (window), "xtext");
if (!xtext)
return;
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
gtk_xtext_set_palette (GTK_XTEXT (xtext), xtext_palette);
}
static void
key_dialog_theme_changed (const ThemeChangedEvent *event, gpointer userdata)
{
GtkWidget *window = userdata;
if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) &&
!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK) &&
!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE))
return;
key_dialog_theme_apply (window);
}
static void
key_dialog_theme_destroy_cb (GtkWidget *widget, gpointer userdata)
{
guint listener_id;
(void) userdata;
listener_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), KEY_DIALOG_THEME_LISTENER_ID_KEY));
if (listener_id)
{
theme_listener_unregister (listener_id);
g_object_set_data (G_OBJECT (widget), KEY_DIALOG_THEME_LISTENER_ID_KEY, NULL);
}
}
static gsize static gsize
key_action_decode_escapes (const char *input, char *output) key_action_decode_escapes (const char *input, char *output)
{ {
@@ -362,69 +321,6 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
"ACCEL=F1\nMenu Shortcut\nD1:contents\nD2!\n\n"\ "ACCEL=F1\nMenu Shortcut\nD1:contents\nD2!\n\n"\
"ACCEL=<Primary><Shift>T\nReopen Closed Tab\nD1!\nD2!\n\n" "ACCEL=<Primary><Shift>T\nReopen Closed Tab\nD1!\nD2!\n\n"
static gboolean
key_builtin_data_match (char *line, char *data)
{
if (line[2] == '!')
return data == NULL || data[0] == 0;
if (line[2] == ':')
return !strcmp (&line[3], data ? data : "");
return FALSE;
}
static gboolean
key_binding_is_builtin (struct key_binding *kb)
{
char *buf, *ibuf;
char *action;
int pnt = 0;
int state = 0;
gboolean match = FALSE;
gboolean d1_match = FALSE;
off_t size;
if (kb->action < 0 || kb->action > KEY_MAX_ACTIONS)
return FALSE;
action = key_actions[kb->action].name;
ibuf = g_strdup (default_kb_cfg);
size = strlen (default_kb_cfg);
while (buf_get_line (ibuf, &buf, &pnt, size))
{
if (strlen (buf) == 0)
continue;
switch (state)
{
case 0:
state = 1;
break;
case 1:
match = !strcmp (buf, action);
state = 2;
break;
case 2:
d1_match = match && key_builtin_data_match (buf, kb->data1);
state = 3;
break;
case 3:
if (d1_match && key_builtin_data_match (buf, kb->data2))
{
g_free (ibuf);
return TRUE;
}
state = 0;
break;
}
}
g_free (ibuf);
return FALSE;
}
void void
key_init (void) key_init (void)
{ {
@@ -570,6 +466,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:
@@ -610,13 +512,13 @@ key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod)
enum enum
{ {
KEY_COLUMN, KEY_COLUMN = PREFERENCES_SHORTCUT_LABEL,
ACCEL_COLUMN, ACCEL_COLUMN = PREFERENCES_SHORTCUT_ACCEL,
ACTION_COLUMN, ACTION_COLUMN = PREFERENCES_SHORTCUT_ACTION,
D1_COLUMN, D1_COLUMN = PREFERENCES_SHORTCUT_DATA1,
D2_COLUMN, D2_COLUMN = PREFERENCES_SHORTCUT_DATA2,
CUSTOM_COLUMN, CUSTOM_COLUMN = PREFERENCES_SHORTCUT_CUSTOM,
N_COLUMNS N_COLUMNS = PREFERENCES_SHORTCUT_N_COLUMNS
}; };
static GtkWidget *key_dialog = NULL; static GtkWidget *key_dialog = NULL;
@@ -624,280 +526,15 @@ static GtkWidget *key_dialog = NULL;
static inline GtkTreeModel * static inline GtkTreeModel *
get_store (void) get_store (void)
{ {
return gtk_tree_view_get_model (g_object_get_data (G_OBJECT (key_dialog), "view")); return g_object_get_data (G_OBJECT (key_dialog), "bindings-store");
}
static void
key_dialog_print_text (GtkXText *xtext, char *text)
{
unsigned int old = prefs.hex_stamp_text;
prefs.hex_stamp_text = 0; /* temporarily disable stamps */
gtk_xtext_clear (GTK_XTEXT (xtext)->buffer, 0);
PrintTextRaw (GTK_XTEXT (xtext)->buffer, text, 0, 0);
prefs.hex_stamp_text = old;
}
static void
key_dialog_set_key (GtkCellRendererAccel *accel, gchar *pathstr, guint accel_key,
GdkModifierType accel_mods, guint hardware_keycode, gpointer userdata)
{
GtkTreeModel *model = get_store ();
GtkTreePath *path = gtk_tree_path_new_from_string (pathstr);
GtkTreeIter iter;
gchar *label_name, *accel_name;
/* Shift tab requires an exception, hopefully that list ends here.. */
if (accel_key == GDK_KEY_Tab && accel_mods & GDK_SHIFT_MASK)
accel_key = GDK_KEY_ISO_Left_Tab;
label_name = gtk_accelerator_get_label (accel_key, key_modifier_get_valid (accel_mods));
accel_name = gtk_accelerator_name (accel_key, key_modifier_get_valid (accel_mods));
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, KEY_COLUMN, label_name,
ACCEL_COLUMN, accel_name, -1);
gtk_tree_path_free (path);
g_free (label_name);
g_free (accel_name);
}
static void
key_dialog_combo_changed (GtkCellRendererCombo *combo, gchar *pathstr,
GtkTreeIter *new_iter, gpointer data)
{
GtkTreeModel *model;
GtkXText *xtext;
gchar *actiontext = NULL;
gint action;
xtext = GTK_XTEXT (g_object_get_data (G_OBJECT (key_dialog), "xtext"));
model = GTK_TREE_MODEL (data);
gtk_tree_model_get (model, new_iter, 0, &actiontext, -1);
if (actiontext)
{
#ifdef WIN32
/* We need to manually update the store */
GtkTreePath *path;
GtkTreeIter iter;
path = gtk_tree_path_new_from_string (pathstr);
model = get_store ();
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, ACTION_COLUMN, actiontext, -1);
gtk_tree_path_free (path);
#endif
action = key_get_action_from_string (actiontext);
key_dialog_print_text (xtext, key_actions[action].help);
g_free (actiontext);
}
}
static void
key_dialog_entry_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpointer data)
{
GtkTreeModel *model = get_store ();
GtkTreePath *path = gtk_tree_path_new_from_string (pathstr);
GtkTreeIter iter;
gint column = GPOINTER_TO_INT (data);
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, column, new_text, -1);
gtk_tree_path_free (path);
}
static gboolean
key_dialog_keypress (GtkWidget *wid, GdkEventKey *evt, gpointer userdata)
{
GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
GtkTreeModel *store;
GtkTreeIter iter1, iter2;
GtkTreeSelection *sel;
GtkTreePath *path;
gboolean handled = FALSE;
int delta;
if (evt->state & GDK_SHIFT_MASK)
{
if (evt->keyval == GDK_KEY_Up)
{
handled = TRUE;
delta = -1;
}
else if (evt->keyval == GDK_KEY_Down)
{
handled = TRUE;
delta = 1;
}
}
if (handled)
{
gboolean custom1, custom2;
sel = gtk_tree_view_get_selection (view);
if (!gtk_tree_selection_get_selected (sel, &store, &iter1))
return FALSE;
path = gtk_tree_model_get_path (store, &iter1);
if (delta == 1)
gtk_tree_path_next (path);
else if (!gtk_tree_path_prev (path))
{
gtk_tree_path_free (path);
return FALSE;
}
if (!gtk_tree_model_get_iter (store, &iter2, path))
{
gtk_tree_path_free (path);
return FALSE;
}
gtk_tree_path_free (path);
gtk_tree_model_get (store, &iter1, CUSTOM_COLUMN, &custom1, -1);
gtk_tree_model_get (store, &iter2, CUSTOM_COLUMN, &custom2, -1);
if (custom1 && custom2)
gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
}
return handled;
}
static void
key_dialog_selection_changed (GtkTreeSelection *sel, gpointer userdata)
{
GtkTreeModel *model;
GtkTreeIter iter;
GtkXText *xtext;
GtkWidget *delete_button;
char *actiontext;
gboolean custom;
int action;
delete_button = g_object_get_data (G_OBJECT (key_dialog), "delete_button");
if (!gtk_tree_selection_get_selected (sel, &model, &iter) || model == NULL)
{
if (delete_button)
gtk_widget_set_sensitive (delete_button, FALSE);
return;
}
xtext = GTK_XTEXT (g_object_get_data (G_OBJECT (key_dialog), "xtext"));
gtk_tree_model_get (model, &iter, ACTION_COLUMN, &actiontext, CUSTOM_COLUMN, &custom, -1);
if (delete_button)
gtk_widget_set_sensitive (delete_button, custom);
if (actiontext)
{
action = key_get_action_from_string (actiontext);
key_dialog_print_text (xtext, key_actions[action].help);
g_free (actiontext);
}
else
key_dialog_print_text (xtext, _("Select a row to get help information on its Action."));
}
static void
key_dialog_close (GtkWidget *wid, gpointer userdata)
{
gtk_widget_destroy (key_dialog);
key_dialog = NULL;
}
static void
key_dialog_save (GtkWidget *wid, gpointer userdata)
{
GtkTreeModel *store = get_store ();
GtkTreeIter iter;
struct key_binding *kb;
char *data1, *data2, *accel, *actiontext;
guint keyval;
GdkModifierType mod;
if (keybind_list)
{
g_slist_free_full (keybind_list, key_free);
keybind_list = NULL;
}
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter))
{
do
{
kb = g_new0 (struct key_binding, 1);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, ACCEL_COLUMN, &accel,
ACTION_COLUMN, &actiontext,
D1_COLUMN, &data1,
D2_COLUMN, &data2,
-1);
kb->data1 = data1;
kb->data2 = data2;
if (accel)
{
gtk_accelerator_parse (accel, &keyval, &mod);
kb->keyval = keyval;
kb->mod = key_modifier_get_valid (mod);
g_free (accel);
}
if (actiontext)
{
kb->action = key_get_action_from_string (actiontext);
g_free (actiontext);
}
if (!accel || !actiontext)
key_free (kb);
else
keybind_list = g_slist_append (keybind_list, kb);
}
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter));
}
if (key_save_kbs () == 0)
{
menu_update_quit_accel ();
key_dialog_close (wid, NULL);
}
}
static void
key_dialog_add (GtkWidget *wid, gpointer userdata)
{
GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
GtkTreeViewColumn *col;
GtkListStore *store = GTK_LIST_STORE (get_store ());
GtkTreeIter iter;
GtkTreePath *path;
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, CUSTOM_COLUMN, TRUE, -1);
/* make sure the new row is visible and selected */
path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
col = gtk_tree_view_get_column (view, ACTION_COLUMN);
gtk_tree_view_scroll_to_cell (view, path, NULL, FALSE, 0.0, 0.0);
gtk_tree_view_set_cursor (view, path, col, TRUE);
gtk_tree_path_free (path);
} }
static char * static char *
key_binding_signature (const char *action, const char *data1, const char *data2) key_binding_signature (const char *action, const char *data1, const char *data2)
{ {
return g_strdup_printf ("%s\\n%s\\n%s", action ? action : "", data1 ? data1 : "", data2 ? data2 : ""); const char *a = action ? action : "", *b = data1 ? data1 : "", *c = data2 ? data2 : "";
return g_strdup_printf ("%" G_GSIZE_FORMAT ":%s%" G_GSIZE_FORMAT ":%s%" G_GSIZE_FORMAT ":%s",
strlen (a), a, strlen (b), b, strlen (c), c);
} }
static int static int
@@ -1022,309 +659,152 @@ key_dialog_reset (GtkWidget *wid, gpointer userdata)
g_slist_free_full (list, key_free); g_slist_free_full (list, key_free);
} }
static void
key_dialog_delete (GtkWidget *wid, gpointer userdata)
{
GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
GtkListStore *store = GTK_LIST_STORE (gtk_tree_view_get_model (view));
GtkTreeIter iter;
GtkTreePath *path;
gboolean custom;
if (gtkutil_treeview_get_selected (view, &iter, -1))
{
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, CUSTOM_COLUMN, &custom, -1);
if (!custom)
return;
/* delete this row, select next one */
if (gtk_list_store_remove (store, &iter))
{
path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), &iter);
gtk_tree_view_scroll_to_cell (view, path, NULL, TRUE, 1.0, 0.0);
gtk_tree_view_set_cursor (view, path, NULL, FALSE);
gtk_tree_path_free (path);
}
}
}
static GtkWidget *
key_dialog_treeview_new (GtkWidget *box)
{
GtkWidget *scroll;
GtkListStore *store, *combostore;
GtkTreeViewColumn *col;
GtkWidget *view;
GtkCellRenderer *render;
int i;
scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
g_return_val_if_fail (store != NULL, NULL);
view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (view), TRUE);
gtk_tree_view_set_enable_search (GTK_TREE_VIEW (view), FALSE);
gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view), FALSE);
g_signal_connect (G_OBJECT (view), "key-press-event",
G_CALLBACK (key_dialog_keypress), NULL);
g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW(view))),
"changed", G_CALLBACK (key_dialog_selection_changed), NULL);
gtk_widget_set_name (view, "fkeys-treeview");
if (!theme_gtk3_is_active ())
{
GtkCssProvider *provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (
provider,
"treeview#fkeys-treeview row:nth-child(odd) {"
" background-color: @theme_base_color;"
"}"
"treeview#fkeys-treeview row:nth-child(even) {"
" background-color: shade(@theme_base_color, 0.96);"
"}",
-1,
NULL);
theme_css_apply_widget_provider (view, GTK_STYLE_PROVIDER (provider));
g_object_unref (provider);
}
render = gtk_cell_renderer_accel_new ();
g_object_set (render, "editable", TRUE,
#ifndef WIN32
"accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER,
#endif
NULL);
g_signal_connect (G_OBJECT (render), "accel-edited",
G_CALLBACK (key_dialog_set_key), NULL);
gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (view), KEY_COLUMN,
"Key", render,
"text", KEY_COLUMN,
NULL);
render = gtk_cell_renderer_text_new ();
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (view), ACCEL_COLUMN,
"Accel", render,
"text", ACCEL_COLUMN,
NULL);
combostore = gtk_list_store_new (1, G_TYPE_STRING);
for (i = 0; i <= KEY_MAX_ACTIONS; i++)
{
GtkTreeIter iter;
if (key_actions[i].name[0])
{
gtk_list_store_append (combostore, &iter);
gtk_list_store_set (combostore, &iter, 0, key_actions[i].name, -1);
}
}
render = gtk_cell_renderer_combo_new ();
g_object_set (G_OBJECT (render), "model", combostore,
"has-entry", FALSE,
"editable", TRUE,
"text-column", 0,
NULL);
g_signal_connect (G_OBJECT (render), "edited",
G_CALLBACK (key_dialog_entry_edited), GINT_TO_POINTER (ACTION_COLUMN));
g_signal_connect (G_OBJECT (render), "changed",
G_CALLBACK (key_dialog_combo_changed), combostore);
gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (view), ACTION_COLUMN,
"Action", render,
"text", ACTION_COLUMN,
"editable", CUSTOM_COLUMN,
NULL);
render = gtk_cell_renderer_text_new ();
g_object_set (render, "editable", TRUE, NULL);
g_signal_connect (G_OBJECT (render), "edited",
G_CALLBACK (key_dialog_entry_edited), GINT_TO_POINTER (D1_COLUMN));
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (view), D1_COLUMN,
"Data1", render,
"text", D1_COLUMN,
"editable", CUSTOM_COLUMN,
NULL);
render = gtk_cell_renderer_text_new ();
g_object_set (render, "editable", TRUE, NULL);
g_signal_connect (G_OBJECT (render), "edited",
G_CALLBACK (key_dialog_entry_edited), GINT_TO_POINTER (D2_COLUMN));
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (view), D2_COLUMN,
"Data2", render,
"text", D2_COLUMN,
"editable", CUSTOM_COLUMN,
NULL);
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), KEY_COLUMN);
gtk_tree_view_column_set_fixed_width (col, 200);
gtk_tree_view_column_set_resizable (col, TRUE);
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), ACCEL_COLUMN);
gtk_tree_view_column_set_visible (col, FALSE);
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), ACTION_COLUMN);
gtk_tree_view_column_set_fixed_width (col, 160);
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), D1_COLUMN);
gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
gtk_tree_view_column_set_min_width (col, 80);
gtk_tree_view_column_set_resizable (col, TRUE);
col = gtk_tree_view_get_column (GTK_TREE_VIEW (view), D2_COLUMN);
gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
gtk_tree_view_column_set_min_width (col, 80);
gtk_tree_view_column_set_resizable (col, TRUE);
gtk_container_add (GTK_CONTAINER (scroll), view);
gtk_box_pack_start (GTK_BOX (box), scroll, TRUE, TRUE, 0);
return view;
}
static void static void
key_dialog_load (GtkListStore *store) key_dialog_load (GtkListStore *store)
{ {
struct key_binding *kb = NULL; GHashTable *defaults = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
char *label_text, *accel_text; GHashTable *seen = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
GtkTreeIter iter; GSList *reference = NULL;
GSList *list = keybind_list; key_load_kbs_from_buffer (g_strdup (default_kb_cfg), strlen (default_kb_cfg), &reference);
for (GSList *item = reference; item; item = item->next)
while (list)
{ {
kb = (struct key_binding*)list->data; struct key_binding *kb = item->data;
key_dialog_reset_increment (defaults, key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2));
label_text = gtk_accelerator_get_label (kb->keyval, kb->mod);
accel_text = gtk_accelerator_name (kb->keyval, kb->mod);
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter,
KEY_COLUMN, label_text,
ACCEL_COLUMN, accel_text,
ACTION_COLUMN, key_actions[kb->action].name,
D1_COLUMN, kb->data1,
D2_COLUMN, kb->data2,
CUSTOM_COLUMN, !key_binding_is_builtin (kb), -1);
g_free (accel_text);
g_free (label_text);
list = g_slist_next (list);
} }
/* Protect only the built-in number of occurrences of an action and its
* parameters. Extra copies remain custom after save/reload, including
* copies whose only difference is their key. No runtime order changes. */
for (GSList *item = keybind_list; item; item = item->next)
{
struct key_binding *kb = item->data;
GtkTreeIter iter;
char *signature = key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2);
gboolean custom = key_dialog_reset_count (seen, signature) >= key_dialog_reset_count (defaults, signature);
key_dialog_reset_increment (seen, signature);
char *label = gtk_accelerator_get_label (kb->keyval, kb->mod);
char *accel = gtk_accelerator_name (kb->keyval, kb->mod);
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, KEY_COLUMN, label, ACCEL_COLUMN, accel,
ACTION_COLUMN, key_actions[kb->action].name, D1_COLUMN, kb->data1,
D2_COLUMN, kb->data2, CUSTOM_COLUMN, custom, -1);
g_free (label); g_free (accel);
}
g_slist_free_full (reference, key_free);
g_hash_table_destroy (defaults); g_hash_table_destroy (seen);
}
static char *key_preferences_serialize (GtkTreeModel *model, GError **error);
static void
key_preferences_restore (gpointer unused)
{
key_dialog_reset (NULL, NULL);
}
GtkWidget *
key_preferences_create (void)
{
GtkListStore *store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
PreferencesShortcutAction actions[KEY_MAX_ACTIONS + 1];
for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
{
actions[i].id = key_actions[i].name;
actions[i].help = key_actions[i].help;
}
key_dialog_load (store);
key_dialog = preferences_shortcuts_page_new (store, actions, G_N_ELEMENTS (actions),
key_modifier_get_valid, key_preferences_restore, NULL);
g_object_add_weak_pointer (G_OBJECT (key_dialog), (gpointer *)&key_dialog);
g_object_set_data_full (G_OBJECT (key_dialog), "original-bindings",
key_preferences_serialize (GTK_TREE_MODEL (store), NULL), g_free);
g_object_unref (store);
return key_dialog;
} }
void void
key_dialog_show (void) key_dialog_show (void)
{ {
GtkWidget *vbox, *box, *pane, *scroll; setup_open_page ("keyboard");
GtkWidget *view, *xtext, *delete_button;
GtkListStore *store;
XTextColor xtext_palette[XTEXT_COLS];
char buf[128];
if (key_dialog)
{
mg_bring_tofront (key_dialog);
return;
}
g_snprintf(buf, sizeof(buf), _("Keyboard Shortcuts - %s"), _(DISPLAY_NAME));
key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close,
NULL, 600, 360, &vbox, 0);
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_paned_pack1 (GTK_PANED (pane), box, TRUE, FALSE);
view = key_dialog_treeview_new (box);
scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 80);
gtk_paned_pack2 (GTK_PANED (pane), scroll, TRUE, FALSE);
gtk_paned_set_position (GTK_PANED (pane), 220);
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
xtext = gtk_xtext_new (xtext_palette, 0);
gtk_container_add (GTK_CONTAINER (scroll), xtext);
gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font);
g_object_set_data (G_OBJECT (key_dialog), "view", view);
g_object_set_data (G_OBJECT (key_dialog), "xtext", xtext);
g_object_set_data (G_OBJECT (key_dialog), KEY_DIALOG_THEME_LISTENER_ID_KEY,
GUINT_TO_POINTER (theme_listener_register ("fkeys.window", key_dialog_theme_changed, key_dialog)));
g_signal_connect (G_OBJECT (key_dialog), "destroy", G_CALLBACK (key_dialog_theme_destroy_cb), NULL);
box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
gtk_button_box_set_layout (GTK_BUTTON_BOX (box), GTK_BUTTONBOX_SPREAD);
gtk_box_pack_start (GTK_BOX (vbox), box, FALSE, FALSE, 2);
gtk_container_set_border_width (GTK_CONTAINER (box), 5);
gtkutil_button (box, ICON_FKEYS_NEW, NULL, key_dialog_add,
NULL, _("Add"));
delete_button = gtkutil_button (box, ICON_FKEYS_DELETE, NULL, key_dialog_delete,
NULL, _("Delete"));
g_object_set_data (G_OBJECT (key_dialog), "delete_button", delete_button);
gtk_widget_set_sensitive (delete_button, FALSE);
gtkutil_button (box, ICON_FKEYS_RESET, NULL, key_dialog_reset,
NULL, _("Reset"));
gtkutil_button (box, ICON_FKEYS_CANCEL, NULL, key_dialog_close,
NULL, _("Cancel"));
gtkutil_button (box, ICON_FKEYS_SAVE, NULL, key_dialog_save,
NULL, _("Save"));
store = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (view)));
key_dialog_load (store);
gtk_widget_show_all (key_dialog);
} }
static int static char *
key_save_kbs (void) key_preferences_serialize (GtkTreeModel *model, GError **error)
{ {
int fd; GtkTreeIter iter;
char buf[512]; GString *text = g_string_new ("# ZoiteChat key bindings config file\n\n");
char *accel_text; gboolean valid = gtk_tree_model_get_iter_first (model, &iter);
GSList *list = keybind_list; while (valid)
struct key_binding *kb;
fd = zoitechat_open_file ("keybindings.conf", O_CREAT | O_TRUNC | O_WRONLY,
0x180, XOF_DOMODE);
if (fd < 0)
return 1;
write (fd, buf, g_snprintf (buf, 510, "# ZoiteChat key bindings config file\n\n"));
while (list)
{ {
kb = list->data; char *accel, *action, *data1, *data2;
guint key;
accel_text = gtk_accelerator_name (kb->keyval, kb->mod); GdkModifierType modifiers;
gtk_tree_model_get (model, &iter, ACCEL_COLUMN, &accel, ACTION_COLUMN, &action,
g_snprintf (buf, 510, "ACCEL=%s\n%s\n", accel_text, key_actions[kb->action].name); D1_COLUMN, &data1, D2_COLUMN, &data2, -1);
write (fd, buf, strlen (buf)); if ((!accel || !*accel) && (!action || !*action) && (!data1 || !*data1) && (!data2 || !*data2))
g_free (accel_text); goto next;
gtk_accelerator_parse (accel ? accel : "", &key, &modifiers);
if (kb->data1 && kb->data1[0]) if (!key || !action || strcmp (key_actions[key_get_action_from_string (action)].name, action) != 0 ||
write (fd, buf, g_snprintf (buf, 510, "D1:%s\n", kb->data1)); (data1 && strpbrk (data1, "\r\n")) || (data2 && strpbrk (data2, "\r\n")))
else {
write (fd, "D1!\n", 4); GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
if (kb->data2 && kb->data2[0]) setup_reveal_widget (GTK_WIDGET (view));
write (fd, buf, g_snprintf (buf, 510, "D2:%s\n", kb->data2)); preferences_shortcuts_reveal_path (key_dialog, path);
else gtk_widget_grab_focus (GTK_WIDGET (view));
write (fd, "D2!\n", 4); gtk_tree_path_free (path);
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
write (fd, "\n", 1); _("Complete each keyboard shortcut with a key and an action. Data fields must stay on one line."));
g_free (accel); g_free (action); g_free (data1); g_free (data2);
list = g_slist_next (list); g_string_free (text, TRUE);
return NULL;
} }
g_string_append_printf (text, "ACCEL=%s\n%s\n", accel, action);
if (data1 && *data1) g_string_append_printf (text, "D1:%s\n", data1);
else g_string_append (text, "D1!\n");
if (data2 && *data2) g_string_append_printf (text, "D2:%s\n", data2);
else g_string_append (text, "D2!\n");
g_string_append_c (text, '\n');
next:
g_free (accel); g_free (action); g_free (data1); g_free (data2);
valid = gtk_tree_model_iter_next (model, &iter);
}
return g_string_free (text, FALSE);
}
close (fd); gboolean
return 0; key_preferences_stage (GError **error)
{
char *contents;
const char *original;
if (!key_dialog) return TRUE;
contents = key_preferences_serialize (get_store (), error);
if (!contents) return FALSE;
original = g_object_get_data (G_OBJECT (key_dialog), "original-bindings");
g_object_set_data_full (G_OBJECT (key_dialog), "pending-bindings", NULL, g_free);
if (g_strcmp0 (contents, original) != 0)
{
preferences_persistence_stage_file ("keybindings.conf", contents);
g_object_set_data_full (G_OBJECT (key_dialog), "pending-bindings", g_strdup (contents), g_free);
}
g_free (contents);
return TRUE;
}
void
key_preferences_commit (void)
{
GSList *bindings = NULL;
const char *contents;
if (!key_dialog) return;
contents = g_object_get_data (G_OBJECT (key_dialog), "pending-bindings");
if (!contents) return;
if (key_load_kbs_from_buffer (g_strdup (contents), strlen (contents), &bindings) == 0)
{
g_slist_free_full (keybind_list, key_free);
keybind_list = bindings;
menu_update_quit_accel ();
}
} }
#define KBSTATE_MOD 0 #define KBSTATE_MOD 0
@@ -1333,6 +813,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 +1050,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;
+6
View File
@@ -31,10 +31,16 @@
void key_init (void); void key_init (void);
void key_dialog_show (void); void key_dialog_show (void);
GtkWidget *key_preferences_create (void);
gboolean key_preferences_stage (GError **error);
void key_preferences_commit (void);
int key_handle_key_press (GtkWidget * wid, GdkEventKey * evt, session *sess); int key_handle_key_press (GtkWidget * wid, GdkEventKey * evt, session *sess);
int key_action_insert (GtkWidget * wid, GdkEventKey * evt, char *d1, char *d2, 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
+22 -18
View File
@@ -31,6 +31,7 @@
#include "../common/fe.h" #include "../common/fe.h"
#include "gtkutil.h" #include "gtkutil.h"
#include "maingui.h" #include "maingui.h"
#include "setup.h"
#define ICON_IGNORE_NEW "document-new" #define ICON_IGNORE_NEW "document-new"
#define ICON_IGNORE_DELETE "edit-delete" #define ICON_IGNORE_DELETE "edit-delete"
@@ -234,6 +235,8 @@ ignore_delete_entry_clicked (GtkWidget * wid, struct session *sess)
static void static void
ignore_store_new (int cancel, char *mask, gpointer data) ignore_store_new (int cancel, char *mask, gpointer data)
{ {
if (cancel || !ignorewin)
return;
GtkTreeView *view = g_object_get_data (G_OBJECT (ignorewin), "view"); GtkTreeView *view = g_object_get_data (G_OBJECT (ignorewin), "view");
GtkListStore *store = GTK_LIST_STORE (get_store ()); GtkListStore *store = GTK_LIST_STORE (get_store ());
GtkTreeIter iter; GtkTreeIter iter;
@@ -265,6 +268,11 @@ ignore_store_new (int cancel, char *mask, gpointer data)
static void static void
ignore_clear_cb (GtkDialog *dialog, gint response) ignore_clear_cb (GtkDialog *dialog, gint response)
{ {
if (!ignorewin)
{
gtk_widget_destroy (GTK_WIDGET (dialog));
return;
}
GtkListStore *store = GTK_LIST_STORE (get_store ()); GtkListStore *store = GTK_LIST_STORE (get_store ());
GtkTreeIter iter; GtkTreeIter iter;
char *mask; char *mask;
@@ -293,7 +301,7 @@ ignore_clear_entry_clicked (GtkWidget * wid)
{ {
GtkWidget *dialog; GtkWidget *dialog;
dialog = gtk_message_dialog_new (NULL, 0, dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (ignorewin)), GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
_("Are you sure you want to remove all ignores?")); _("Are you sure you want to remove all ignores?"));
theme_manager_attach_window (dialog); theme_manager_attach_window (dialog);
@@ -335,8 +343,8 @@ ignore_stats_entry (GtkWidget * box, char *label, int value)
return wid; return wid;
} }
void GtkWidget *
ignore_gui_open (void) ignore_preferences_create (void)
{ {
GtkWidget *vbox, *box, *stat_box, *frame; GtkWidget *vbox, *box, *stat_box, *frame;
GtkWidget *view; GtkWidget *view;
@@ -345,20 +353,9 @@ ignore_gui_open (void)
GSList *temp = ignore_list; GSList *temp = ignore_list;
char *mask; char *mask;
gboolean private, chan, notice, ctcp, dcc, invite, unignore; gboolean private, chan, notice, ctcp, dcc, invite, unignore;
char buf[128]; vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
ignorewin = vbox;
if (ignorewin) g_signal_connect (vbox, "destroy", G_CALLBACK (close_ignore_gui_callback), NULL);
{
mg_bring_tofront (ignorewin);
return;
}
g_snprintf(buf, sizeof(buf), _("Ignore list - %s"), _(DISPLAY_NAME));
ignorewin =
mg_create_generic_tab ("IgnoreList", buf, FALSE, TRUE,
close_ignore_gui_callback,
NULL, 700, 300, &vbox, 0);
gtkutil_destroy_on_esc (ignorewin);
view = ignore_treeview_new (vbox); view = ignore_treeview_new (vbox);
g_object_set_data (G_OBJECT (ignorewin), "view", view); g_object_set_data (G_OBJECT (ignorewin), "view", view);
@@ -421,7 +418,14 @@ ignore_gui_open (void)
temp = temp->next; temp = temp->next;
} }
gtk_widget_show (ignorewin); gtk_widget_show_all (ignorewin);
return vbox;
}
void
ignore_gui_open (void)
{
setup_open_page ("ignore");
} }
void void
+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
+121 -164
View File
@@ -50,7 +50,11 @@
#include "banlist.h" #include "banlist.h"
#include "chanlist.h" #include "chanlist.h"
#include "editlist.h" #include "editlist.h"
#include "setup.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"
@@ -1318,24 +1322,24 @@ menu_open_server_list (GtkWidget *wid, gpointer none)
static void static void
menu_settings (GtkWidget * wid, gpointer none) menu_settings (GtkWidget * wid, gpointer none)
{ {
extern void setup_open (void);
setup_open (); setup_open ();
} }
#ifdef __APPLE__
static void static void
menu_usermenu (void) menu_macos_minimize (GtkWidget *widget, gpointer userdata)
{ {
char buf[128]; GtkWindow *fallback = NULL;
g_snprintf(buf, sizeof(buf), _("User menu - %s"), _(DISPLAY_NAME)); if (current_sess && current_sess->gui && current_sess->gui->window)
editlist_gui_open (NULL, NULL, usermenu_list, buf, "usermenu", "usermenu.conf", 0); fallback = GTK_WINDOW (current_sess->gui->window);
macos_shortcuts_minimize_active (fallback);
} }
#endif
static void static void
usermenu_create (GtkWidget *menu) usermenu_create (GtkWidget *menu)
{ {
menu_create (menu, usermenu_list, "", FALSE); menu_create (menu, usermenu_list, "", FALSE);
menu_quick_item (0, 0, menu, XCMENU_SHADED, 0, 0); /* sep */
menu_quick_item_with_callback (menu_usermenu, _("Edit This Menu" ELLIPSIS), menu, 0);
} }
static void static void
@@ -1644,33 +1648,6 @@ menu_banlist (GtkWidget * wid, gpointer none)
banlist_opengui (current_sess); banlist_opengui (current_sess);
} }
#ifdef USE_PLUGIN
static void
menu_loadplugin (void)
{
plugingui_load ();
}
static void
menu_pluginlist (void)
{
plugingui_open ();
}
#else
static void
menu_noplugin_info (void)
{
fe_message (_(DISPLAY_NAME " has been build without plugin support."), FE_MSG_INFO);
}
#define menu_loadplugin menu_noplugin_info
#define menu_pluginlist menu_noplugin_info
#endif
#define usercommands_help _("User Commands - Special codes:\n\n"\ #define usercommands_help _("User Commands - Special codes:\n\n"\
"%c = current channel\n"\ "%c = current channel\n"\
"%e = current network name\n"\ "%e = current network name\n"\
@@ -1727,73 +1704,47 @@ menu_noplugin_info (void)
"shell instead of ZoiteChat") "shell instead of ZoiteChat")
static void static void
menu_usercommands (void) menu_preferences_selector_changed (GtkComboBox *combo, GtkStack *stack)
{ {
char buf[128]; const char *id = gtk_combo_box_get_active_id (combo);
g_snprintf(buf, sizeof(buf), _("User Defined Commands - %s"), _(DISPLAY_NAME)); if (id) gtk_stack_set_visible_child_name (stack, id);
editlist_gui_open (NULL, NULL, command_list, buf, "commands", "commands.conf",
usercommands_help);
} }
static void GtkWidget *
menu_ulpopup (void) menu_preferences_create (const char *id)
{ {
char buf[128]; if (!strcmp (id, "replace"))
g_snprintf(buf, sizeof(buf), _("Userlist Popup menu - %s"), _(DISPLAY_NAME)); return editlist_preferences_create (&replace_list, "replace.conf", _("Text"), _("Replace with"), NULL);
editlist_gui_open (NULL, NULL, popup_list, buf, "popup", "popup.conf", ulbutton_help); if (!strcmp (id, "commands"))
} return editlist_preferences_create (&command_list, "commands.conf", _("Command"), _("Expansion"), usercommands_help);
if (!strcmp (id, "urls"))
static void return editlist_preferences_create (&urlhandler_list, "urlhandlers.conf", _("Name"), _("Command"), url_help);
menu_rpopup (void) if (!strcmp (id, "ctcp"))
{ return editlist_preferences_create (&ctcp_list, "ctcpreply.conf", _("Request"), _("Reply"), ctcp_help);
char buf[128]; {
g_snprintf(buf, sizeof(buf), _("Replace - %s"), _(DISPLAY_NAME)); GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
editlist_gui_open (_("Text"), _("Replace with"), replace_list, buf, "replace", "replace.conf", 0); GtkWidget *selector = gtk_combo_box_text_new ();
} GtkWidget *stack = gtk_stack_new ();
const char *ids[] = {"buttons", "popup", "dlgbuttons", "usermenu"};
static void const char *names[] = {N_("User list buttons"), N_("User list popup"), N_("Dialog buttons"), N_("User menu")};
menu_urlhandlers (void) const char *files[] = {"buttons.conf", "popup.conf", "dlgbuttons.conf", "usermenu.conf"};
{ GSList **lists[] = {&button_list, &popup_list, &dlgbutton_list, &usermenu_list};
char buf[128]; const char *help[] = {ulbutton_help, ulbutton_help, dlgbutton_help, ulbutton_help};
g_snprintf(buf, sizeof(buf), _("URL Handlers - %s"), _(DISPLAY_NAME)); int i;
editlist_gui_open (NULL, NULL, urlhandler_list, buf, "urlhandlers", "urlhandlers.conf", url_help); gtk_box_pack_start (GTK_BOX (box), selector, FALSE, FALSE, 0);
} gtk_box_pack_start (GTK_BOX (box), stack, TRUE, TRUE, 0);
gtk_stack_set_homogeneous (GTK_STACK (stack), FALSE);
static void for (i = 0; i < 4; i++)
menu_evtpopup (void) {
{ gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (selector), ids[i], _(names[i]));
pevent_dialog_show (); gtk_stack_add_named (GTK_STACK (stack), editlist_preferences_create (lists[i], files[i], _("Name"), _("Command"), help[i]), ids[i]);
} }
g_signal_connect (selector, "changed", G_CALLBACK (menu_preferences_selector_changed), stack);
static void gtk_combo_box_set_active (GTK_COMBO_BOX (selector), 0);
menu_keypopup (void) g_object_set_data (G_OBJECT (box), "menus-selector", selector);
{ g_object_set_data (G_OBJECT (stack), "preferences-selector", selector);
key_dialog_show (); return box;
} }
static void
menu_ulbuttons (void)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("Userlist buttons - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, button_list, buf, "buttons", "buttons.conf", ulbutton_help);
}
static void
menu_dlgbuttons (void)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("Dialog buttons - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, dlgbutton_list, buf, "dlgbuttons", "dlgbuttons.conf",
dlgbutton_help);
}
static void
menu_ctcpguiopen (void)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("CTCP Replies - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, ctcp_list, buf, "ctcpreply", "ctcpreply.conf", ctcp_help);
} }
static void static void
@@ -1986,7 +1937,7 @@ menu_about (GtkWidget *wid, gpointer sess)
static struct mymenu mymenu[] = { static struct mymenu mymenu[] = {
{N_("_ZoiteChat"), 0, 0, M_NEWMENU, MENU_ID_ZOITECHAT, 0, 1}, {N_("_ZoiteChat"), 0, 0, M_NEWMENU, MENU_ID_ZOITECHAT, 0, 1},
{N_("Network Li_st"), menu_open_server_list, 0, M_MENUITEM, 0, 0, 1}, {N_("Network Li_st" ELLIPSIS), menu_open_server_list, 0, M_MENUITEM, 0, 0, 1},
{0, 0, 0, M_SEP, 0, 0, 0}, {0, 0, 0, M_SEP, 0, 0, 0},
{N_("_New"), 0, 0, M_MENUSUB, 0, 0, 1}, {N_("_New"), 0, 0, M_MENUSUB, 0, 0, 1},
@@ -1997,14 +1948,14 @@ static struct mymenu mymenu[] = {
{0, 0, 0, M_END, 0, 0, 0}, {0, 0, 0, M_END, 0, 0, 0},
{0, 0, 0, M_SEP, 0, 0, 0}, {0, 0, 0, M_SEP, 0, 0, 0},
{N_("_Load Plugin or Script" ELLIPSIS), menu_loadplugin, 0, M_MENUITEM, 0, 0, 1}, #define DETACH_OFFSET (10)
{0, 0, 0, M_SEP, 0, 0, 0}, /* 11 */ {0, menu_detach, 0, M_MENUITEM, 0, 0, 1},
#define DETACH_OFFSET (12) #define CLOSE_OFFSET (11)
{0, menu_detach, 0, M_MENUITEM, 0, 0, 1}, /* 12 */
#define CLOSE_OFFSET (13)
{0, menu_close, 0, M_MENUITEM, 0, 0, 1}, {0, menu_close, 0, M_MENUITEM, 0, 0, 1},
{0, 0, 0, M_SEP, 0, 0, 0}, {0, 0, 0, M_SEP, 0, 0, 0},
{N_("_Quit"), menu_quit, 0, M_MENUITEM, MENU_ID_QUIT, 0, 1}, /* 15 */ {N_("_Preferences" ELLIPSIS), menu_settings, 0, M_MENUITEM, 0, 0, 1},
{0, 0, 0, M_SEP, 0, 0, 0},
{N_("_Quit"), menu_quit, 0, M_MENUITEM, MENU_ID_QUIT, 0, 1},
{N_("_View"), 0, 0, M_NEWMENU, 0, 0, 1}, {N_("_View"), 0, 0, M_NEWMENU, 0, 0, 1},
#define MENUBAR_OFFSET (17) #define MENUBAR_OFFSET (17)
@@ -2014,18 +1965,18 @@ static struct mymenu mymenu[] = {
{N_("U_ser List Buttons"), menu_ulbuttons_toggle, 0, M_MENUTOG, MENU_ID_ULBUTTONS, 0, 1}, {N_("U_ser List Buttons"), menu_ulbuttons_toggle, 0, M_MENUTOG, MENU_ID_ULBUTTONS, 0, 1},
{N_("M_ode Buttons"), menu_cmbuttons_toggle, 0, M_MENUTOG, MENU_ID_MODEBUTTONS, 0, 1}, {N_("M_ode Buttons"), menu_cmbuttons_toggle, 0, M_MENUTOG, MENU_ID_MODEBUTTONS, 0, 1},
{0, 0, 0, M_SEP, 0, 0, 0}, {0, 0, 0, M_SEP, 0, 0, 0},
{N_("_Channel Switcher"), 0, 0, M_MENUSUB, 0, 0, 1}, /* 23 */ {N_("_Channel Switcher"), 0, 0, M_MENUSUB, 0, 0, 1},
#define TABS_OFFSET (24) #define TABS_OFFSET (24)
{N_("_Tabs"), menu_layout_cb, 0, M_MENURADIO, MENU_ID_LAYOUT_TABS, 0, 1}, {N_("_Tabs"), menu_layout_cb, 0, M_MENURADIO, MENU_ID_LAYOUT_TABS, 0, 1},
{N_("T_ree"), 0, 0, M_MENURADIO, MENU_ID_LAYOUT_TREE, 0, 1}, {N_("T_ree"), 0, 0, M_MENURADIO, MENU_ID_LAYOUT_TREE, 0, 1},
{0, 0, 0, M_END, 0, 0, 0}, {0, 0, 0, M_END, 0, 0, 0},
{N_("_Network Meters"), 0, 0, M_MENUSUB, 0, 0, 1}, /* 27 */ {N_("_Network Meters"), 0, 0, M_MENUSUB, 0, 0, 1},
#define METRE_OFFSET (28) #define METRE_OFFSET (28)
{N_("Off"), menu_metres_off, 0, M_MENURADIO, 0, 0, 1}, {N_("Off"), menu_metres_off, 0, M_MENURADIO, 0, 0, 1},
{N_("Graph"), menu_metres_graph, 0, M_MENURADIO, 0, 0, 1}, {N_("Graph"), menu_metres_graph, 0, M_MENURADIO, 0, 0, 1},
{N_("Text"), menu_metres_text, 0, M_MENURADIO, 0, 0, 1}, {N_("Text"), menu_metres_text, 0, M_MENURADIO, 0, 0, 1},
{N_("Both"), menu_metres_both, 0, M_MENURADIO, 0, 0, 1}, {N_("Both"), menu_metres_both, 0, M_MENURADIO, 0, 0, 1},
{0, 0, 0, M_END, 0, 0, 0}, /* 32 */ {0, 0, 0, M_END, 0, 0, 0},
{ 0, 0, 0, M_SEP, 0, 0, 0 }, { 0, 0, 0, M_SEP, 0, 0, 0 },
{N_ ("_Fullscreen"), menu_fullscreen_toggle, 0, M_MENUTOG, MENU_ID_FULLSCREEN, 0, 1}, {N_ ("_Fullscreen"), menu_fullscreen_toggle, 0, M_MENUTOG, MENU_ID_FULLSCREEN, 0, 1},
@@ -2038,20 +1989,7 @@ static struct mymenu mymenu[] = {
#define AWAY_OFFSET (41) #define AWAY_OFFSET (41)
{N_("Marked _Away"), menu_away_toggle, 0, M_MENUITEM, MENU_ID_AWAY, 0, 1}, {N_("Marked _Away"), menu_away_toggle, 0, M_MENUITEM, MENU_ID_AWAY, 0, 1},
{N_("_Usermenu"), 0, 0, M_NEWMENU, MENU_ID_USERMENU, 0, 1}, /* 40 */ {N_("_Usermenu"), 0, 0, M_NEWMENU, MENU_ID_USERMENU, 0, 1},
{N_("S_ettings"), 0, 0, M_NEWMENU, 0, 0, 1},
{N_("_Preferences"), menu_settings, 0, M_MENUITEM, 0, 0, 1},
{0, 0, 0, M_SEP, 0, 0, 0},
{N_("Auto Replace"), menu_rpopup, 0, M_MENUITEM, 0, 0, 1},
{N_("CTCP Replies"), menu_ctcpguiopen, 0, M_MENUITEM, 0, 0, 1},
{N_("Dialog Buttons"), menu_dlgbuttons, 0, M_MENUITEM, 0, 0, 1},
{N_("Keyboard Shortcuts"), menu_keypopup, 0, M_MENUITEM, 0, 0, 1},
{N_("Text Events"), menu_evtpopup, 0, M_MENUITEM, 0, 0, 1},
{N_("URL Handlers"), menu_urlhandlers, 0, M_MENUITEM, 0, 0, 1},
{N_("User Commands"), menu_usercommands, 0, M_MENUITEM, 0, 0, 1},
{N_("User List Buttons"), menu_ulbuttons, 0, M_MENUITEM, 0, 0, 1},
{N_("User List Popup"), menu_ulpopup, 0, M_MENUITEM, 0, 0, 1}, /* 52 */
{N_("_Window"), 0, 0, M_NEWMENU, 0, 0, 1}, {N_("_Window"), 0, 0, M_NEWMENU, 0, 0, 1},
{N_("_Ban List"), menu_banlist, 0, M_MENUITEM, 0, 0, 1}, {N_("_Ban List"), menu_banlist, 0, M_MENUITEM, 0, 0, 1},
@@ -2059,9 +1997,7 @@ static struct mymenu mymenu[] = {
{N_("Direct Chat"), menu_dcc_chat_win, 0, M_MENUITEM, 0, 0, 1}, {N_("Direct Chat"), menu_dcc_chat_win, 0, M_MENUITEM, 0, 0, 1},
{N_("File _Transfers"), menu_dcc_win, 0, M_MENUITEM, 0, 0, 1}, {N_("File _Transfers"), menu_dcc_win, 0, M_MENUITEM, 0, 0, 1},
{N_("Friends List"), notify_opengui, 0, M_MENUITEM, 0, 0, 1}, {N_("Friends List"), notify_opengui, 0, M_MENUITEM, 0, 0, 1},
{N_("Ignore List"), ignore_gui_open, 0, M_MENUITEM, 0, 0, 1}, {N_("_Raw Log"), menu_rawlog, 0, M_MENUITEM, 0, 0, 1},
{N_("_Plugins and Scripts"), menu_pluginlist, 0, M_MENUITEM, 0, 0, 1},
{N_("_Raw Log"), menu_rawlog, 0, M_MENUITEM, 0, 0, 1}, /* 61 */
{N_("_URL Grabber"), url_opengui, 0, M_MENUITEM, 0, 0, 1}, {N_("_URL Grabber"), url_opengui, 0, M_MENUITEM, 0, 0, 1},
{0, 0, 0, M_SEP, 0, 0, 0}, {0, 0, 0, M_SEP, 0, 0, 0},
{N_("Reset Marker Line"), menu_resetmarker, 0, M_MENUITEM, 0, 0, 1}, {N_("Reset Marker Line"), menu_resetmarker, 0, M_MENUITEM, 0, 0, 1},
@@ -2069,14 +2005,14 @@ 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 (57)
{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},
{N_("Search Previous" ), menu_search_prev, 0, M_MENUITEM, 0, 0, 1}, {N_("Search Previous" ), menu_search_prev, 0, M_MENUITEM, 0, 0, 1},
{0, 0, 0, M_END, 0, 0, 0}, {0, 0, 0, M_END, 0, 0, 0},
{N_("_Help"), 0, 0, M_NEWMENU, 0, 0, 1}, /* 74 */ {N_("_Help"), 0, 0, M_NEWMENU, 0, 0, 1},
{N_("_Contents"), menu_docs, 0, M_MENUITEM, 0, 0, 1}, {N_("_Contents"), menu_docs, 0, M_MENUITEM, 0, 0, 1},
{N_("_About"), menu_about, 0, M_MENUITEM, 0, 0, 1}, {N_("_About"), menu_about, 0, M_MENUITEM, 0, 0, 1},
@@ -2086,42 +2022,24 @@ static struct mymenu mymenu[] = {
static const char * static const char *
menu_get_key_action_name (int index) menu_get_key_action_name (int index)
{ {
switch (index) /* Bind accelerators to actions rather than positions in the menu. */
{ void *callback = mymenu[index].callback;
case 1: if (callback == menu_open_server_list) return "network-list";
return "network-list"; if (callback == menu_newserver_tab) return "new-server-tab";
case 4: if (callback == menu_newserver_window) return "new-server-window";
return "new-server-tab"; if (callback == menu_close) return "close";
case 6: if (callback == menu_quit) return "quit";
return "new-server-window"; if (callback == menu_bar_toggle_cb) return "menu-toggle";
case CLOSE_OFFSET: if (callback == menu_userlist_toggle) return "user-list-toggle";
return "close"; if (callback == menu_fullscreen_toggle) return "fullscreen-toggle";
case 15: if (callback == menu_away_toggle) return "away-toggle";
return "quit"; if (callback == menu_resetmarker) return "reset-marker";
case MENUBAR_OFFSET: if (callback == menu_movetomarker) return "move-marker";
return "menu-toggle"; if (callback == menu_copy_selection) return "copy-selection";
case MENUBAR_OFFSET + 2: if (callback == menu_search) return "search-text";
return "user-list-toggle"; if (callback == menu_search_next) return "search-next";
case 34: if (callback == menu_search_prev) return "search-previous";
return "fullscreen-toggle"; if (callback == menu_docs) return "contents";
case AWAY_OFFSET:
return "away-toggle";
case 65:
return "reset-marker";
case 66:
return "move-marker";
case 67:
return "copy-selection";
case SEARCH_OFFSET + 1:
return "search-text";
case SEARCH_OFFSET + 2:
return "search-next";
case SEARCH_OFFSET + 3:
return "search-previous";
case 75:
return "contents";
}
return NULL; return NULL;
} }
@@ -2142,6 +2060,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 +2096,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 +2155,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 +2784,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 +2807,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))
@@ -2955,7 +2912,7 @@ togitem:
#ifdef HAVE_GTK_MAC #ifdef HAVE_GTK_MAC
/* We want ZoiteChat to be the app menu, not including Quit or ZoiteChat itself */ /* We want ZoiteChat to be the app menu, not including Quit or ZoiteChat itself */
if (bar && item && i <= CLOSE_OFFSET + 1 && mymenu[i].id != MENU_ID_ZOITECHAT) if (bar && item && (i <= CLOSE_OFFSET + 1 || mymenu[i].callback == menu_settings) && mymenu[i].id != MENU_ID_ZOITECHAT)
{ {
if (!submenu || mymenu[i].type == M_MENUSUB) if (!submenu || mymenu[i].type == M_MENUSUB)
gtkosx_application_insert_app_menu_item (osx_app, item, appmenu_offset++); gtkosx_application_insert_app_menu_item (osx_app, item, appmenu_offset++);
+1
View File
@@ -42,6 +42,7 @@ void menu_bar_toggle (void);
void menu_add_plugin_items (GtkWidget *menu, char *root, char *target); void menu_add_plugin_items (GtkWidget *menu, char *root, char *target);
void menu_change_layout (void); void menu_change_layout (void);
void menu_update_quit_accel (void); void menu_update_quit_accel (void);
GtkWidget *menu_preferences_create (const char *id);
gboolean menu_key_action (const char *name, guint keyval, GdkModifierType state); gboolean menu_key_action (const char *name, guint keyval, GdkModifierType state);
void menu_set_away (session_gui *gui, int away); void menu_set_away (session_gui *gui, int away);
+50 -3
View File
@@ -32,6 +32,7 @@ zoitechat_gtk_sources = [
'notifygui.c', 'notifygui.c',
'pixmaps.c', 'pixmaps.c',
'preferences-persistence.c', 'preferences-persistence.c',
'preferences-pages.c',
'plugin-tray.c', 'plugin-tray.c',
'plugin-notification.c', 'plugin-notification.c',
'rawlog.c', 'rawlog.c',
@@ -62,7 +63,8 @@ if host_machine.system() == 'darwin' and gtk_mac_dep.found()
gtk_dep = declare_dependency(dependencies: [gtk_dep, gtk_mac_dep]) gtk_dep = declare_dependency(dependencies: [gtk_dep, gtk_mac_dep])
endif endif
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep] theme_math_dep = cc.find_library('m', required: false)
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep, theme_math_dep]
if host_machine.system() != 'windows' if host_machine.system() != 'windows'
appindicator_opt = get_option('appindicator') appindicator_opt = get_option('appindicator')
@@ -109,6 +111,7 @@ if host_machine.system() == 'windows'
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'])
zoitechat_gtk_objcflags += '-fobjc-arc' zoitechat_gtk_objcflags += '-fobjc-arc'
@@ -242,6 +245,50 @@ theme_runtime_tests = executable('theme_runtime_persistence_tests',
link_with: zoitechat_theme_lib, link_with: zoitechat_theme_lib,
) )
preferences_persistence_tests = executable('preferences_persistence_tests',
'tests/test-preferences-persistence.c',
include_directories: [config_h_include],
dependencies: [gtk_dep],
)
test('Preferences Persistence Tests', preferences_persistence_tests,
protocol: 'tap',
timeout: 120,
)
# g_test_skip() was added in GLib 2.38, below GTK 3.22's minimum GLib
# requirement. Allow it in this test target without changing the app's API
# ceiling or lowering macOS's existing 2.60 ceiling.
preferences_pages_test_cflags = []
if host_machine.system() != 'darwin'
preferences_pages_test_cflags = [
'-UGLIB_VERSION_MAX_ALLOWED',
'-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_38',
]
endif
preferences_pages_tests = executable('preferences_pages_tests',
[textevents, 'tests/test-preferences-pages.c', 'preferences-pages.c'],
include_directories: [config_h_include, include_directories('../common')],
dependencies: [gtk_dep],
c_args: preferences_pages_test_cflags,
)
test('Preferences Pages Tests', preferences_pages_tests,
protocol: 'tap',
timeout: 120,
)
spell_entry_tests = executable('spell_entry_tests',
[marshal, 'tests/test-spell-entry.c'],
include_directories: [config_h_include, include_directories('../common')],
dependencies: [gtk_dep, libgmodule_dep],
c_args: preferences_pages_test_cflags,
)
test('Spell Entry Tests', spell_entry_tests,
protocol: 'tap',
timeout: 120,
)
test('Theme Runtime Persistence Tests', theme_runtime_tests, test('Theme Runtime Persistence Tests', theme_runtime_tests,
protocol: 'tap', protocol: 'tap',
timeout: 120, timeout: 120,
@@ -276,7 +323,7 @@ test('Theme GTK3 Settings Tests', theme_gtk3_settings_tests,
theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests', theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests',
'theme/tests/test-theme-preferences-gtk3-populate.c', 'theme/tests/test-theme-preferences-gtk3-populate.c',
include_directories: [config_h_include], include_directories: [config_h_include],
dependencies: [gtk_dep], dependencies: [gtk_dep, theme_math_dep],
) )
test('Theme Preferences GTK3 Populate Tests', theme_preferences_gtk3_populate_tests, test('Theme Preferences GTK3 Populate Tests', theme_preferences_gtk3_populate_tests,
@@ -296,7 +343,7 @@ 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],
) )
+14 -14
View File
@@ -35,6 +35,8 @@ typedef struct session zoitechat_context;
#include "../common/cfgfiles.h" #include "../common/cfgfiles.h"
#include "gtkutil.h" #include "gtkutil.h"
#include "maingui.h" #include "maingui.h"
#include "setup.h"
#include "plugingui.h"
/* model for the plugin treeview */ /* model for the plugin treeview */
enum enum
@@ -357,24 +359,15 @@ plugingui_reloadbutton_cb (GtkWidget *wid, GtkTreeView *view)
} }
} }
void GtkWidget *
plugingui_open (void) plugingui_preferences_create (void)
{ {
GtkWidget *view; GtkWidget *view;
GtkWidget *view_scroll; GtkWidget *view_scroll;
GtkWidget *vbox, *hbox; GtkWidget *vbox, *hbox;
char buf[128]; vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
plugin_window = vbox;
if (plugin_window) g_signal_connect (vbox, "destroy", G_CALLBACK (plugingui_close), NULL);
{
mg_bring_tofront (plugin_window);
return;
}
g_snprintf(buf, sizeof(buf), _("Plugins and Scripts - %s"), _(DISPLAY_NAME));
plugin_window = mg_create_generic_tab ("Addons", buf, FALSE, TRUE, plugingui_close, NULL,
700, 300, &vbox, 0);
gtkutil_destroy_on_esc (plugin_window);
view = plugingui_treeview_new (vbox); view = plugingui_treeview_new (vbox);
view_scroll = gtk_widget_get_parent (view); view_scroll = gtk_widget_get_parent (view);
@@ -404,4 +397,11 @@ plugingui_open (void)
fe_pluginlist_update (); fe_pluginlist_update ();
gtk_widget_show_all (plugin_window); gtk_widget_show_all (plugin_window);
return vbox;
}
void
plugingui_open (void)
{
setup_open_page ("plugins");
} }
+1
View File
@@ -21,6 +21,7 @@
#define ZOITECHAT_PLUGINGUI_H #define ZOITECHAT_PLUGINGUI_H
void plugingui_open (void); void plugingui_open (void);
GtkWidget *plugingui_preferences_create (void);
void plugingui_load (void); void plugingui_load (void);
#endif #endif
File diff suppressed because it is too large. Load diff
+62
View File
@@ -0,0 +1,62 @@
/* ZoiteChat - native editors for the everyday preferences pages.
* Licensed under the GNU General Public License, version 2 or later.
*/
#ifndef ZOITECHAT_PREFERENCES_PAGES_H
#define ZOITECHAT_PREFERENCES_PAGES_H
#include <gtk/gtk.h>
struct zoitechatprefs;
typedef enum
{
PREFERENCES_ALERT_DESKTOP = 1 << 0,
PREFERENCES_ALERT_TRAY = 1 << 1,
PREFERENCES_ALERT_ATTENTION = 1 << 2,
PREFERENCES_ALERT_TRAY_BEHAVIOR = 1 << 3,
PREFERENCES_ALERT_HIDDEN_ONLY = 1 << 4
} PreferencesAlertCapabilities;
typedef void (*PreferencesSoundChanged) (gpointer user_data);
/* All editors write only to the window's staged preferences. The caller owns
* both the preferences and the staged Beep event's allocated filename. */
GtkWidget *preferences_text_page_new (struct zoitechatprefs *staged,
const char *const *languages);
GtkWidget *preferences_tabs_page_new (struct zoitechatprefs *staged);
GtkWidget *preferences_notifications_page_new (struct zoitechatprefs *staged,
PreferencesAlertCapabilities capabilities, char **beep_file,
PreferencesSoundChanged sound_changed, gpointer user_data);
void preferences_notifications_refresh_sound (GtkWidget *page);
GtkWidget *preferences_history_page_new (struct zoitechatprefs *staged,
const char *config_dir);
gboolean preferences_history_page_stage (GtkWidget *page, GError **error);
typedef struct
{
const char *code;
const char *name;
} PreferencesSpellLanguage;
typedef void (*PreferencesTypingPreview) (GtkWidget *entry, const struct zoitechatprefs *staged);
GtkWidget *preferences_typing_page_new (struct zoitechatprefs *staged, GtkWidget *preview_entry,
const PreferencesSpellLanguage *languages, guint count, PreferencesTypingPreview update_preview);
gboolean preferences_typing_page_stage (GtkWidget *page, GError **error);
/* These columns retain the existing keybindings.conf editor's model and order. */
enum
{
PREFERENCES_SHORTCUT_LABEL, PREFERENCES_SHORTCUT_ACCEL,
PREFERENCES_SHORTCUT_ACTION, PREFERENCES_SHORTCUT_DATA1,
PREFERENCES_SHORTCUT_DATA2, PREFERENCES_SHORTCUT_CUSTOM,
PREFERENCES_SHORTCUT_N_COLUMNS
};
typedef struct { const char *id; const char *help; } PreferencesShortcutAction;
typedef GdkModifierType (*PreferencesShortcutModifiers) (GdkModifierType modifiers);
typedef void (*PreferencesShortcutsRestore) (gpointer data);
GtkWidget *preferences_shortcuts_page_new (GtkListStore *store,
const PreferencesShortcutAction *actions, guint count,
PreferencesShortcutModifiers normalize, PreferencesShortcutsRestore restore, gpointer data);
void preferences_shortcuts_reveal_path (GtkWidget *page, GtkTreePath *raw_path);
void preferences_shortcuts_find_action (GtkWidget *page, const char *action_id);
#endif
+214 -51
View File
@@ -23,64 +23,227 @@
#include "../common/cfgfiles.h" #include "../common/cfgfiles.h"
#include "theme/theme-runtime.h" #include "theme/theme-runtime.h"
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <glib/gstdio.h>
#ifdef WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
typedef struct
{
char *name;
char *contents;
char *temporary;
char *backup;
gboolean existed;
gboolean installed;
} PreferencesFile;
static GPtrArray *staged_files;
static void
preferences_file_free (gpointer data)
{
PreferencesFile *file = data;
if (file->temporary) g_unlink (file->temporary);
if (file->backup) g_unlink (file->backup);
g_free (file->name);
g_free (file->contents);
g_free (file->temporary);
g_free (file->backup);
g_free (file);
}
void
preferences_persistence_clear_staged_files (void)
{
if (staged_files) g_ptr_array_free (staged_files, TRUE);
staged_files = NULL;
}
void
preferences_persistence_stage_file (const char *name, const char *contents)
{
PreferencesFile *file;
g_return_if_fail (name && contents && !strchr (name, '/') && !strchr (name, '\\'));
if (!staged_files)
staged_files = g_ptr_array_new_with_free_func (preferences_file_free);
file = g_new0 (PreferencesFile, 1);
file->name = g_strdup (name);
file->contents = g_strdup (contents);
g_ptr_array_add (staged_files, file);
}
/* A temporary in the configuration directory stays on the same filesystem.
* Restrictive permissions also protect command text and identity settings. */
static gboolean
preferences_write_temporary (const char *contents, gsize length, char **path)
{
int fd;
gsize written = 0;
char *temporary = g_build_filename (get_xdir (), ".preferences-XXXXXX", NULL);
fd = g_mkstemp (temporary);
if (fd < 0) { g_free (temporary); return FALSE; }
while (written < length)
{
gssize count = write (fd, contents + written, length - written);
if (count < 0 && errno == EINTR) continue;
if (count <= 0)
{
close (fd);
g_unlink (temporary);
g_free (temporary);
return FALSE;
}
written += count;
}
if (close (fd) != 0)
{
g_unlink (temporary);
g_free (temporary);
return FALSE;
}
*path = temporary;
return TRUE;
}
static gboolean
preferences_backup (PreferencesFile *file)
{
char *path = g_build_filename (get_xdir (), file->name, NULL);
char *contents = NULL;
gsize length;
GError *error = NULL;
gboolean success;
if (!g_file_get_contents (path, &contents, &length, &error))
{
success = g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT);
g_clear_error (&error);
g_free (path);
return success;
}
file->existed = TRUE;
success = preferences_write_temporary (contents, length, &file->backup);
g_free (contents);
g_free (path);
return success;
}
static gboolean
preferences_install (PreferencesFile *file, const char *temporary)
{
char *path = g_build_filename (get_xdir (), file->name, NULL);
gboolean success;
#ifdef WIN32
/* Match the existing config/theme save convention on Windows. The
* backup remains available if replacement fails after unlinking. */
if (file->existed && g_unlink (path) != 0)
{
g_free (path);
return FALSE;
}
file->installed = TRUE;
#endif
success = g_rename (temporary, path) == 0;
if (success) file->installed = TRUE;
g_free (path);
return success;
}
static gboolean
preferences_restore (PreferencesFile *file)
{
char *path;
gboolean success;
if (!file->installed) return TRUE;
path = g_build_filename (get_xdir (), file->name, NULL);
if (file->existed)
{
#ifdef WIN32
g_unlink (path);
#endif
success = g_rename (file->backup, path) == 0;
/* Retain the recovery copy if restoration itself fails. */
if (!success) g_clear_pointer (&file->backup, g_free);
}
else success = g_unlink (path) == 0 || errno == ENOENT;
g_free (path);
return success;
}
PreferencesPersistenceResult PreferencesPersistenceResult
preferences_persistence_save_all (void) preferences_persistence_save_all (void)
{ {
PreferencesPersistenceResult result; PreferencesPersistenceResult result = {0};
char *config_temp; GPtrArray *files = g_ptr_array_new_with_free_func (preferences_file_free);
char *theme_temp; PreferencesFile *config = g_new0 (PreferencesFile, 1);
PreferencesFile *theme = g_new0 (PreferencesFile, 1);
result.success = FALSE; guint i;
result.retry_possible = TRUE; result.retry_possible = TRUE;
result.partial_failure = FALSE; config->name = g_strdup ("zoitechat.conf");
result.config_failed = FALSE; theme->name = g_strdup ("colors.conf");
result.theme_failed = FALSE; g_ptr_array_add (files, config);
result.failed_file = NULL; g_ptr_array_add (files, theme);
config_temp = NULL; if (!save_config_prepare (&config->temporary))
theme_temp = NULL;
if (!save_config_prepare (&config_temp))
{ {
result.config_failed = TRUE; result.config_failed = TRUE;
goto done;
}
if (!theme_runtime_save_prepare (&theme_temp))
{
result.theme_failed = TRUE;
goto done;
}
if (!save_config_finalize (config_temp))
{
result.config_failed = TRUE;
goto done;
}
if (!theme_runtime_save_finalize (theme_temp))
{
result.theme_failed = TRUE;
result.partial_failure = TRUE;
goto done;
}
result.success = TRUE;
done:
if (!result.success)
{
if (result.config_failed && result.theme_failed)
result.failed_file = "zoitechat.conf and colors.conf";
else if (result.config_failed)
result.failed_file = "zoitechat.conf"; result.failed_file = "zoitechat.conf";
else if (result.theme_failed) goto done;
result.failed_file = "colors.conf";
} }
if (!theme_runtime_save_prepare (&theme->temporary))
save_config_discard (config_temp); {
theme_runtime_save_discard (theme_temp); result.theme_failed = TRUE;
g_free (config_temp); result.failed_file = "colors.conf";
g_free (theme_temp); goto done;
}
for (i = 0; staged_files && i < staged_files->len; i++)
{
PreferencesFile *queued = g_ptr_array_index (staged_files, i);
PreferencesFile *file = g_new0 (PreferencesFile, 1);
file->name = g_strdup (queued->name);
g_ptr_array_add (files, file);
if (!preferences_write_temporary (queued->contents, strlen (queued->contents), &file->temporary))
{
result.failed_file = queued->name;
goto done;
}
}
/* Prepare every file and recovery copy before replacing anything. */
for (i = 0; i < files->len; i++)
{
PreferencesFile *file = g_ptr_array_index (files, i);
if (!preferences_backup (file))
{
result.failed_file = i < 2 ? (i ? "colors.conf" : "zoitechat.conf")
: ((PreferencesFile *)g_ptr_array_index (staged_files, i - 2))->name;
result.config_failed = i == 0;
result.theme_failed = i == 1;
goto done;
}
}
for (i = 0; i < files->len; i++)
{
PreferencesFile *file = g_ptr_array_index (files, i);
if (!preferences_install (file, file->temporary))
{
result.failed_file = i < 2 ? (i ? "colors.conf" : "zoitechat.conf")
: ((PreferencesFile *)g_ptr_array_index (staged_files, i - 2))->name;
result.config_failed = i == 0;
result.theme_failed = i == 1;
goto rollback;
}
}
result.success = TRUE;
goto done;
rollback:
for (i = files->len; i > 0; i--)
if (!preferences_restore (g_ptr_array_index (files, i - 1)))
result.partial_failure = TRUE;
done:
g_ptr_array_free (files, TRUE);
return result; return result;
} }
+4
View File
@@ -34,5 +34,9 @@ typedef struct
} PreferencesPersistenceResult; } PreferencesPersistenceResult;
PreferencesPersistenceResult preferences_persistence_save_all (void); PreferencesPersistenceResult preferences_persistence_save_all (void);
/* Editors queue serialized settings; runtime state is committed only after
* every queued file and the main preferences have been saved successfully. */
void preferences_persistence_stage_file (const char *name, const char *contents);
void preferences_persistence_clear_staged_files (void);
#endif #endif
+16 -14
View File
@@ -70,7 +70,7 @@ static GtkWidget *entry_nick1;
static GtkWidget *entry_nick2; static GtkWidget *entry_nick2;
static GtkWidget *entry_nick3; static GtkWidget *entry_nick3;
static GtkWidget *entry_guser; static GtkWidget *entry_guser;
/* static GtkWidget *entry_greal; */ static GtkWidget *entry_greal;
enum { enum {
SERVER_TREE, SERVER_TREE,
@@ -1650,7 +1650,7 @@ servlist_savegui (void)
sp = strchr (prefs.hex_irc_user_name, ' '); sp = strchr (prefs.hex_irc_user_name, ' ');
if (sp) if (sp)
sp[0] = 0; /* spaces will break the login */ sp[0] = 0; /* spaces will break the login */
/* strcpy (prefs.hex_irc_real_name, gtk_entry_get_text (GTK_ENTRY (entry_greal))); */ safe_strcpy (prefs.hex_irc_real_name, gtk_entry_get_text (GTK_ENTRY (entry_greal)), sizeof(prefs.hex_irc_real_name));
servlist_save (); servlist_save ();
if (!save_config ()) if (!save_config ())
fe_message (_("Could not save zoitechat.conf."), FE_MSG_WARN); fe_message (_("Could not save zoitechat.conf."), FE_MSG_WARN);
@@ -2809,12 +2809,12 @@ servlist_open_networks (void)
GtkWidget *label4; GtkWidget *label4;
GtkWidget *label5; GtkWidget *label5;
GtkWidget *label6; GtkWidget *label6;
/* GtkWidget *label7; */ GtkWidget *label7;
GtkWidget *entry1; GtkWidget *entry1;
GtkWidget *entry2; GtkWidget *entry2;
GtkWidget *entry3; GtkWidget *entry3;
GtkWidget *entry4; GtkWidget *entry4;
/* GtkWidget *entry5; */ GtkWidget *entry5;
GtkWidget *vbox2; GtkWidget *vbox2;
GtkWidget *label1; GtkWidget *label1;
GtkWidget *table4; GtkWidget *table4;
@@ -2900,12 +2900,12 @@ servlist_open_networks (void)
gtk_widget_set_halign (label6, GTK_ALIGN_START); gtk_widget_set_halign (label6, GTK_ALIGN_START);
gtk_widget_set_valign (label6, GTK_ALIGN_CENTER); gtk_widget_set_valign (label6, GTK_ALIGN_CENTER);
/* label7 = gtk_label_new_with_mnemonic (_("Rea_l name:")); label7 = gtk_label_new_with_mnemonic (_("Rea_l name:"));
gtk_widget_show (label7); gtk_widget_show (label7);
gtk_table_attach (GTK_TABLE (table1), label7, 0, 1, 4, 5, servlist_table_attach (table1, label7, 0, 1, 4, 5,
(GtkAttachOptions) (GTK_FILL), FALSE, FALSE,
(GtkAttachOptions) (0), 0, 0); SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
*/ 0, 0);
entry_nick1 = entry1 = gtk_entry_new (); entry_nick1 = entry1 = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry1), prefs.hex_irc_nick1); gtk_entry_set_text (GTK_ENTRY (entry1), prefs.hex_irc_nick1);
@@ -2939,12 +2939,14 @@ servlist_open_networks (void)
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0); 0, 0);
/* entry_greal = entry5 = gtk_entry_new (); entry_greal = entry5 = gtk_entry_new ();
gtk_entry_set_max_length (GTK_ENTRY (entry5), sizeof prefs.hex_irc_real_name - 1);
gtk_entry_set_text (GTK_ENTRY (entry5), prefs.hex_irc_real_name); gtk_entry_set_text (GTK_ENTRY (entry5), prefs.hex_irc_real_name);
gtk_widget_show (entry5); gtk_widget_show (entry5);
gtk_table_attach (GTK_TABLE (table1), entry5, 1, 2, 4, 5, servlist_table_attach (table1, entry5, 1, 2, 4, 5,
(GtkAttachOptions) (GTK_EXPAND | GTK_FILL), TRUE, FALSE,
(GtkAttachOptions) (0), 0, 0); */ SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
vbox2 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); vbox2 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_widget_show (vbox2); gtk_widget_show (vbox2);
@@ -3098,7 +3100,7 @@ button_connect = gtkutil_button (hbuttonbox1, ICON_SERVLIST_CONNECT, NULL,
gtk_label_set_mnemonic_widget (GTK_LABEL (label3), entry1); gtk_label_set_mnemonic_widget (GTK_LABEL (label3), entry1);
gtk_label_set_mnemonic_widget (GTK_LABEL (label6), entry4); gtk_label_set_mnemonic_widget (GTK_LABEL (label6), entry4);
/* gtk_label_set_mnemonic_widget (GTK_LABEL (label7), entry5); */ gtk_label_set_mnemonic_widget (GTK_LABEL (label7), entry5);
gtk_widget_grab_focus (networks_tree); gtk_widget_grab_focus (networks_tree);
gtk_widget_grab_default (button_close); gtk_widget_grab_default (button_close);
+994 -362
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -23,5 +23,10 @@
#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);
/* Stable, untranslated page IDs allow existing menu entries to deep-link. */
void setup_open_page (const char *id);
/* Reveal an existing editor, including any nested menu selector, on errors. */
void setup_reveal_widget (GtkWidget *widget);
#endif #endif
+4 -2
View File
@@ -928,6 +928,10 @@ sexy_spell_entry_init(SexySpellEntry *entry)
entry->priv = g_new0(SexySpellEntryPriv, 1); entry->priv = g_new0(SexySpellEntryPriv, 1);
entry->priv->dict_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); entry->priv->dict_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
entry->priv->attr_list = pango_attr_list_new();
/* An untouched input still needs an empty word list. Loading the first
* working dictionary on Save can recheck it before any changed signal. */
entry->priv->words = g_new0(gchar *, 1);
if (have_enchant) if (have_enchant)
{ {
@@ -939,8 +943,6 @@ sexy_spell_entry_init(SexySpellEntry *entry)
sexy_spell_entry_activate_default_languages(entry); sexy_spell_entry_activate_default_languages(entry);
} }
entry->priv->attr_list = pango_attr_list_new();
entry->priv->checked = TRUE; entry->priv->checked = TRUE;
entry->priv->parseattr = TRUE; entry->priv->parseattr = TRUE;
+633
View File
@@ -0,0 +1,633 @@
/* Native preferences editor regressions. GPL-2.0-or-later. */
#include "../../common/zoitechat.h"
#include <glib/gstdio.h>
#include "../../common/fe.h"
#include "../../common/util.h"
#include "../preferences-pages.h"
#include "../theme/theme-access.h"
#include "../notifications/notification-backend.h"
static gboolean gtk_available, notifications_available;
static int notification_calls, beep_calls, file_calls, sound_changes;
static char *played_file, *opened_page;
static char *opened_folder, *preview_mask;
/* External effects are recorded; tests drive the real native page widgets. */
int notification_backend_supported (void) { return notifications_available; }
void notification_backend_show (const char *title, const char *text) { notification_calls++; }
void fe_beep (session *sess) { beep_calls++; }
void sound_play (const char *file, gboolean quiet)
{
file_calls++;
g_free (played_file);
played_file = g_strdup (file);
}
void setup_open_page (const char *id)
{
g_free (opened_page);
opened_page = g_strdup (id);
}
void setup_reveal_widget (GtkWidget *widget) { }
void fe_open_url (const char *url)
{
g_free (opened_folder);
opened_folder = g_strdup (url);
}
char *log_build_pathname (const char *mask, const char *config_dir,
const char *server, const char *channel,
const char *network, time_t now)
{
/* Record the resolver input and return a sentinel path, so these UI
* tests verify the displayed result comes from the resolver. */
g_free (preview_mask);
preview_mask = g_strdup (mask);
return g_build_filename (config_dir, "resolved-example.log", NULL);
}
gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *rgba)
{
return gdk_rgba_parse (rgba, token == THEME_TOKEN_TEXT_BACKGROUND ? "#20242a" : "#eeeeee");
}
int get_stamp_str (char *format, time_t stamp, char **result)
{
GDateTime *date = g_date_time_new_from_unix_local (stamp);
*result = g_date_time_format (date, format);
g_date_time_unref (date);
return *result ? strlen (*result) : 0;
}
static void changed (gpointer data) { sound_changes++; }
static GtkWidget *
find_widget (GtkWidget *root, const char *name)
{
GtkWidget *found = NULL;
if (!strcmp (gtk_widget_get_name (root), name)) return root;
if (GTK_IS_BUTTON (root) && !g_strcmp0 (gtk_button_get_label (GTK_BUTTON (root)), name)) return root;
if (!GTK_IS_CONTAINER (root)) return NULL;
GList *children = gtk_container_get_children (GTK_CONTAINER (root));
for (GList *i = children; i && !found; i = i->next) found = find_widget (i->data, name);
g_list_free (children);
return found;
}
static GtkWidget *control (GtkWidget *root, const char *name)
{
GtkWidget *widget = find_widget (root, name);
g_assert_nonnull (widget);
return widget;
}
static void
destroy_page (GtkWidget *page)
{
g_object_ref_sink (page);
gtk_widget_destroy (page);
g_object_unref (page);
}
static void
defaults (struct zoitechatprefs *prefs)
{
memset (prefs, 0, sizeof *prefs);
g_strlcpy (prefs->hex_text_font, "Monospace 11", sizeof prefs->hex_text_font);
#ifdef WIN32
g_strlcpy (prefs->hex_text_font_main, "Monospace 11", sizeof prefs->hex_text_font_main);
#endif
g_strlcpy (prefs->hex_stamp_text_format, "[%H:%M:%S] ", sizeof prefs->hex_stamp_text_format);
prefs->hex_gui_tab_layout = 2;
prefs->hex_gui_tab_pos = 3;
prefs->hex_text_indent = 1;
prefs->hex_text_show_marker = 1;
prefs->hex_stamp_text = 1;
}
static void
test_open_preserves_values (void)
{
struct zoitechatprefs prefs, original;
GtkWidget *page;
char *sound = NULL;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
/* Unknown legacy values and formats must survive merely visiting a page. */
prefs.hex_gui_tab_layout = 1;
prefs.hex_gui_tab_pos = 7;
prefs.hex_gui_tab_trunc = 123;
prefs.hex_gui_tab_newtofront = 9;
prefs.hex_text_wrap_align = 9;
g_strlcpy (prefs.hex_stamp_text_format, "时刻 %H:%M ", sizeof prefs.hex_stamp_text_format);
original = prefs;
page = preferences_text_page_new (&prefs, NULL);
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
destroy_page (page);
page = preferences_tabs_page_new (&prefs);
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
destroy_page (page);
page = preferences_notifications_page_new (&prefs, 0, &sound, changed, NULL);
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
g_assert_null (sound);
destroy_page (page);
}
static void
test_timestamp_and_dependencies (void)
{
struct zoitechatprefs prefs;
GtkWidget *page, *style, *format;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
g_strlcpy (prefs.hex_stamp_text_format, "Custom %H:%M ", sizeof prefs.hex_stamp_text_format);
page = preferences_text_page_new (&prefs, NULL);
style = control (page, "timestamp-style"); format = control (page, "timestamp-format");
g_assert_cmpstr (gtk_combo_box_get_active_id (GTK_COMBO_BOX (style)), ==, "custom");
gtk_combo_box_set_active_id (GTK_COMBO_BOX (style), "%H:%M ");
g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "%H:%M ");
g_assert_false (gtk_widget_is_sensitive (format));
gtk_combo_box_set_active_id (GTK_COMBO_BOX (style), "custom");
g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "Custom %H:%M ");
gtk_entry_set_text (GTK_ENTRY (format), "[%H:%M] ");
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "timestamps")), FALSE);
g_assert_false (gtk_widget_is_sensitive (format));
g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "[%H:%M] ");
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "indent-nicks")), FALSE);
g_assert_true (gtk_widget_is_sensitive (control (page, "wrap-alignment")));
gtk_combo_box_set_active (GTK_COMBO_BOX (control (page, "wrap-alignment")), 2);
g_assert_cmpint (prefs.hex_text_wrap_align, ==, 2);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "indent-nicks")), TRUE);
g_assert_false (gtk_widget_is_sensitive (control (page, "wrap-alignment")));
g_assert_cmpint (prefs.hex_text_wrap_align, ==, 2);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "inline-modes")), TRUE);
g_assert_false (gtk_widget_is_sensitive (control (page, "multiline-topic")));
destroy_page (page);
}
static void
test_preview_updates (void)
{
struct zoitechatprefs prefs;
GtkWidget *page;
GtkTextBuffer *buffer;
GtkTextIter start, end;
char *text;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
page = preferences_text_page_new (&prefs, NULL);
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (control (page, "conversation-preview")));
gtk_text_buffer_get_bounds (buffer, &start, &end);
text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
g_assert_nonnull (strstr (text, "[14:35:08]"));
g_assert_nonnull (strstr (text, "Unread messages"));
g_free (text);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "timestamps")), FALSE);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "unread-marker")), FALSE);
gtk_text_buffer_get_bounds (buffer, &start, &end);
text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
g_assert_null (strstr (text, "14:35:08"));
g_assert_null (strstr (text, "Unread messages"));
g_free (text);
destroy_page (page);
}
static void
test_layout_switching (void)
{
struct zoitechatprefs prefs;
GtkWidget *page, *tree_control, *tab_control;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
prefs.hex_gui_tab_closebuttons = 1;
prefs.hex_gui_tab_dots = 1;
page = preferences_tabs_page_new (&prefs);
tree_control = control (page, "tree-dots"); tab_control = control (page, "tab-close-buttons");
g_assert_true (gtk_widget_is_visible (tree_control));
g_assert_false (gtk_widget_is_visible (tab_control));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "layout-tabs")), TRUE);
g_assert_cmpint (prefs.hex_gui_tab_layout, ==, 0);
g_assert_cmpint (prefs.hex_gui_tab_pos, ==, 6);
g_assert_true (gtk_widget_is_visible (tab_control));
g_assert_false (gtk_widget_is_visible (tree_control));
g_assert_cmpint (prefs.hex_gui_tab_dots, ==, 1);
gtk_combo_box_set_active (GTK_COMBO_BOX (control (page, "channel-placement")), 4);
g_assert_cmpint (prefs.hex_gui_tab_pos, ==, 5);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "layout-tree")), TRUE);
g_assert_cmpint (prefs.hex_gui_tab_pos, ==, 3);
g_assert_cmpint (prefs.hex_gui_tab_closebuttons, ==, 1);
destroy_page (page);
}
static void
test_notification_mapping (void)
{
struct zoitechatprefs prefs;
GtkWidget *page;
char *sound = NULL;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
page = preferences_notifications_page_new (&prefs, 31, &sound, changed, NULL);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "mention-desktop")), TRUE);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "private-beep")), TRUE);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "channel-tray")), TRUE);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "private-attention")), TRUE);
g_assert_cmpint (prefs.hex_input_balloon_hilight, ==, 1);
g_assert_cmpint (prefs.hex_input_balloon_chans, ==, 0);
g_assert_cmpint (prefs.hex_input_beep_priv, ==, 1);
g_assert_cmpint (prefs.hex_input_beep_hilight, ==, 0);
g_assert_cmpint (prefs.hex_input_tray_chans, ==, 1);
g_assert_cmpint (prefs.hex_input_tray_priv, ==, 0);
g_assert_cmpint (prefs.hex_input_flash_priv, ==, 1);
g_assert_false (gtk_widget_is_sensitive (control (page, "tray-close")));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "tray-enabled")), TRUE);
g_assert_true (gtk_widget_is_sensitive (control (page, "tray-close")));
destroy_page (page);
}
static void
test_shared_sound_and_effects (void)
{
struct zoitechatprefs prefs;
GtkWidget *page;
char *sound = g_strdup ("original.wav");
if (!gtk_available) { g_free (sound); g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
page = preferences_notifications_page_new (&prefs, 31, &sound, changed, NULL);
sound_changes = beep_calls = file_calls = 0;
gtk_entry_set_text (GTK_ENTRY (control (page, "alert-sound-file")), "new.wav");
g_assert_cmpstr (sound, ==, "new.wav");
g_assert_cmpint (sound_changes, ==, 1);
gtk_button_clicked (GTK_BUTTON (control (page, "Play")));
g_assert_cmpstr (played_file, ==, "new.wav");
gtk_button_clicked (GTK_BUTTON (control (page, "Use system sound")));
g_assert_cmpstr (sound, ==, "");
gtk_button_clicked (GTK_BUTTON (control (page, "Play")));
g_assert_cmpint (beep_calls, ==, 1);
g_free (sound); sound = g_strdup ("changed-in-sounds.wav");
preferences_notifications_refresh_sound (page);
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (control (page, "alert-sound-file"))), ==, sound);
g_assert_cmpint (sound_changes, ==, 2);
gtk_button_clicked (GTK_BUTTON (control (page, "open-event-sounds")));
g_assert_cmpstr (opened_page, ==, "sounds");
destroy_page (page);
g_free (sound);
}
static void
test_notification_test_safety (void)
{
struct zoitechatprefs prefs, original;
GtkWidget *page;
char *sound = NULL;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
prefs.hex_away_omit_alerts = prefs.hex_gui_focus_omitalerts = 1;
original = prefs;
notifications_available = TRUE; notification_calls = 0;
page = preferences_notifications_page_new (&prefs, 31, &sound, changed, NULL);
gtk_button_clicked (GTK_BUTTON (control (page, "notification-test")));
g_assert_cmpint (notification_calls, ==, 1);
g_assert_nonnull (strstr (gtk_label_get_text (GTK_LABEL (control (page, "notification-test-status"))), "marked away"));
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
notifications_available = FALSE;
gtk_button_clicked (GTK_BUTTON (control (page, "notification-test")));
g_assert_cmpint (notification_calls, ==, 1);
destroy_page (page);
page = preferences_notifications_page_new (&prefs, 0, &sound, changed, NULL);
g_assert_false (gtk_widget_is_sensitive (control (page, "notification-test")));
g_assert_false (gtk_widget_is_sensitive (control (page, "private-desktop")));
g_assert_null (find_widget (page, "private-tray"));
g_assert_null (find_widget (page, "private-attention"));
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
destroy_page (page);
}
static void
test_history_preserves_values (void)
{
struct zoitechatprefs prefs, original;
GtkWidget *page;
char *config, *history;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
config = g_dir_make_tmp ("zoitechat-history-ui-XXXXXX", NULL);
history = g_build_filename (config, "scrollback", NULL);
g_strlcpy (prefs.hex_irc_logmask, "custom-%s-%Y-%m.log", sizeof prefs.hex_irc_logmask);
g_strlcpy (prefs.hex_stamp_log_format, "Custom %H:%M ", sizeof prefs.hex_stamp_log_format);
prefs.hex_text_max_lines = 120000;
prefs.hex_url_grabber_limit = 12000;
original = prefs;
page = preferences_history_page_new (&prefs, config);
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
g_assert_false (g_file_test (history, G_FILE_TEST_EXISTS));
g_assert_false (gtk_widget_is_sensitive (control (page, "open-history-folder")));
g_assert_cmpstr (gtk_combo_box_get_active_id (GTK_COMBO_BOX (control (page, "log-file-style"))), ==, "custom");
char *expected = g_build_filename (config, "resolved-example.log", NULL);
g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (control (page, "log-example-file"))), ==, expected);
g_free (expected);
destroy_page (page);
g_rmdir (config);
g_free (config); g_free (history);
}
static void
test_history_presets_and_custom (void)
{
struct zoitechatprefs prefs;
GtkWidget *page, *combo;
char *mask, *expected;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs); prefs.hex_irc_logging = 1;
mask = g_build_filename (g_get_tmp_dir (), "100%%done", "%s-custom.log", NULL);
g_strlcpy (prefs.hex_irc_logmask, mask, sizeof prefs.hex_irc_logmask);
page = preferences_history_page_new (&prefs, g_get_tmp_dir ());
combo = control (page, "log-file-style");
gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 1);
expected = g_build_filename (g_get_tmp_dir (), "100%%done", "%n", "%Y-%m", "%c.log", NULL);
g_assert_cmpstr (prefs.hex_irc_logmask, ==, expected);
g_assert_cmpstr (preview_mask, ==, expected);
gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo), "custom");
g_assert_cmpstr (prefs.hex_irc_logmask, ==, mask);
gtk_button_clicked (GTK_BUTTON (control (page, "default-log-folder")));
g_assert_cmpstr (prefs.hex_irc_logmask, ==, "%s-custom.log");
g_free (expected); g_free (mask);
destroy_page (page);
}
static void
test_history_dependencies_and_timestamps (void)
{
struct zoitechatprefs prefs;
GtkWidget *page, *combo;
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs);
g_strlcpy (prefs.hex_irc_logmask, "%n" G_DIR_SEPARATOR_S "%c.log", sizeof prefs.hex_irc_logmask);
g_strlcpy (prefs.hex_stamp_log_format, "Time %H:%M ", sizeof prefs.hex_stamp_log_format);
prefs.hex_stamp_log = 1;
prefs.hex_url_logging = 1;
prefs.hex_url_grabber_limit = 91;
page = preferences_history_page_new (&prefs, g_get_tmp_dir ());
g_assert_false (gtk_widget_is_sensitive (control (page, "choose-log-folder")));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "save-logs")), TRUE);
g_assert_true (gtk_widget_is_sensitive (control (page, "choose-log-folder")));
combo = control (page, "log-timestamp-style");
gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo), "%H:%M ");
g_assert_cmpstr (prefs.hex_stamp_log_format, ==, "%H:%M ");
g_assert_nonnull (strstr (gtk_label_get_text (GTK_LABEL (control (page, "log-example-line"))), "14:35"));
gtk_combo_box_set_active_id (GTK_COMBO_BOX (combo), "custom");
g_assert_cmpstr (prefs.hex_stamp_log_format, ==, "Time %H:%M ");
g_assert_cmpstr (prefs.hex_stamp_text_format, ==, "[%H:%M:%S] ");
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "log-timestamps")), FALSE);
g_assert_false (gtk_widget_is_sensitive (control (page, "log-timestamp-format")));
g_assert_cmpstr (prefs.hex_stamp_log_format, ==, "Time %H:%M ");
g_assert_false (gtk_widget_is_sensitive (control (page, "link-limit")));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "collect-links")), TRUE);
g_assert_true (gtk_widget_is_sensitive (control (page, "link-limit")));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "collect-links")), FALSE);
g_assert_cmpint (prefs.hex_url_grabber_limit, ==, 91);
g_assert_cmpint (prefs.hex_url_logging, ==, 1);
gtk_button_clicked (GTK_BUTTON (control (page, "open-data-folder")));
g_assert_cmpstr (opened_folder, ==, g_get_tmp_dir ());
destroy_page (page);
}
static void
test_history_validation (void)
{
struct zoitechatprefs prefs;
GtkWidget *page;
GError *error = NULL;
GString *long_format = g_string_new ("");
if (!gtk_available) { g_string_free (long_format, TRUE); g_test_skip ("GTK display unavailable"); return; }
defaults (&prefs); prefs.hex_irc_logging = 1;
g_strlcpy (prefs.hex_irc_logmask, "old-custom.log", sizeof prefs.hex_irc_logmask);
page = preferences_history_page_new (&prefs, g_get_tmp_dir ());
g_assert_true (preferences_history_page_stage (page, &error));
gtk_entry_set_text (GTK_ENTRY (control (page, "log-file-format")), "");
g_assert_false (preferences_history_page_stage (page, &error));
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_clear_error (&error);
for (int i = 0; i < 140; i++) g_string_append (long_format, "é");
gtk_entry_set_text (GTK_ENTRY (control (page, "log-file-format")), long_format->str);
g_assert_false (preferences_history_page_stage (page, &error));
g_clear_error (&error);
gtk_entry_set_text (GTK_ENTRY (control (page, "log-file-format")), "corrected-%Y.log");
g_assert_true (preferences_history_page_stage (page, &error));
g_assert_no_error (error);
destroy_page (page);
g_string_free (long_format, TRUE);
}
static GtkWidget *
language_button (GtkWidget *root, const char *code)
{
GtkWidget *found = NULL;
if (!g_strcmp0 (g_object_get_data (G_OBJECT (root), "language-code"), code)) return root;
if (!GTK_IS_CONTAINER (root)) return NULL;
GList *children = gtk_container_get_children (GTK_CONTAINER (root));
for (GList *i = children; i && !found; i = i->next) found = language_button (i->data, code);
g_list_free (children); return found;
}
static void
test_typing_preservation_and_languages (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
struct zoitechatprefs prefs, original;
defaults (&prefs);
prefs.hex_gui_input_spell = prefs.hex_gui_input_icon = 1;
prefs.hex_completion_amount = 5;
g_strlcpy (prefs.hex_text_spell_langs, "en_US, missing_XX", sizeof prefs.hex_text_spell_langs);
g_strlcpy (prefs.hex_completion_suffix, ",", sizeof prefs.hex_completion_suffix);
original = prefs;
PreferencesSpellLanguage languages[] = {{"en_US", "English (United States)"}, {"en_CA", "English (Canada)"}};
GtkWidget *page = preferences_typing_page_new (&prefs, gtk_entry_new (), languages, 2, NULL);
g_assert_cmpmem (&prefs, sizeof prefs, &original, sizeof original);
g_assert_true (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (language_button (page, "en_US"))));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (language_button (page, "en_CA")), TRUE);
g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "missing_XX"));
g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "en_CA"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (language_button (page, "en_US")), FALSE);
g_assert_null (strstr (prefs.hex_text_spell_langs, "en_US"));
g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "missing_XX"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "spelling-enabled")), FALSE);
g_assert_false (gtk_widget_is_sensitive (language_button (page, "en_CA")));
g_assert_nonnull (strstr (prefs.hex_text_spell_langs, "en_CA"));
g_assert_false (gtk_widget_is_sensitive (control (page, "input-mode-icon")));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (control (page, "input-nickname")), TRUE);
g_assert_true (gtk_widget_is_sensitive (control (page, "input-mode-icon")));
destroy_page (page);
}
static void
test_typing_completion_and_validation (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
struct zoitechatprefs prefs;
defaults (&prefs); prefs.hex_completion_amount = 5;
GtkWidget *page = preferences_typing_page_new (&prefs, gtk_entry_new (), NULL, 0, NULL);
GtkWidget *result = control (page, "completion-example-input");
gtk_entry_set_text (GTK_ENTRY (result), "Al");
gtk_spin_button_set_value (GTK_SPIN_BUTTON (control (page, "completion-limit")), 0);
gtk_entry_set_text (GTK_ENTRY (control (page, "completion-suffix")), "✓");
g_assert_cmpstr (prefs.hex_completion_suffix, ==, "✓");
GError *error = NULL;
g_assert_true (preferences_typing_page_stage (page, &error));
gtk_entry_set_text (GTK_ENTRY (control (page, "completion-suffix")), "😀");
g_assert_false (preferences_typing_page_stage (page, &error));
g_assert_cmpstr (prefs.hex_completion_suffix, ==, "✓");
g_clear_error (&error);
gtk_entry_set_text (GTK_ENTRY (control (page, "completion-suffix")), ":");
char *too_long = g_strnfill (64, 'a');
gtk_entry_set_text (GTK_ENTRY (control (page, "spelling-codes")), too_long);
g_assert_false (preferences_typing_page_stage (page, &error));
g_clear_error (&error); g_free (too_long);
gtk_entry_set_text (GTK_ENTRY (control (page, "spelling-codes")), "unavailable-code");
g_assert_true (preferences_typing_page_stage (page, &error));
g_assert_no_error (error);
destroy_page (page);
}
static GdkModifierType test_modifiers (GdkModifierType mods)
{
return mods & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_META_MASK);
}
static void test_restore (gpointer data) { }
static GtkWidget *
shortcut_fixture (GtkListStore **store)
{
*store = gtk_list_store_new (6, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
GtkTreeIter iter;
for (int i = 0; i < 3; i++)
{
gtk_list_store_append (*store, &iter);
gtk_list_store_set (*store, &iter, 0, "Ctrl+K", 1, "<Control>k", 2, i == 1 ? "Change Page" : "Run Command",
3, i == 0 ? "\002" : i == 1 ? "2" : "echo original\\nsecond", 4, i == 1 ? "Relative-custom-value" : NULL, 5, i != 0, -1);
}
PreferencesShortcutAction actions[] = {{"Run Command", "Run a command"}, {"Change Page", "Change a page"}};
return preferences_shortcuts_page_new (*store, actions, 2, test_modifiers, test_restore, NULL);
}
static void
test_shortcuts_filter_and_protection (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
GtkListStore *store;
GtkWidget *page = shortcut_fixture (&store);
GtkTreeIter iter;
gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
char *data;
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
g_assert_cmpstr (data, ==, "\002"); g_free (data);
g_assert_false (gtk_widget_get_sensitive (control (page, "shortcut-action")));
gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-remove")));
g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 3);
g_assert_nonnull (strstr (gtk_label_get_text (GTK_LABEL (control (page, "shortcut-shared"))), "3 bindings"));
gtk_entry_set_text (GTK_ENTRY (control (page, "shortcut-search")), "Relative-custom-value");
g_signal_emit_by_name (control (page, "shortcut-search"), "search-changed");
GtkTreeModel *filtered = gtk_tree_view_get_model (GTK_TREE_VIEW (control (page, "shortcuts-list")));
g_assert_cmpint (gtk_tree_model_iter_n_children (filtered, NULL), ==, 1);
g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 3);
GtkTreePath *path = gtk_tree_path_new_from_indices (1, -1);
preferences_shortcuts_reveal_path (page, path); gtk_tree_path_free (path);
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (control (page, "shortcut-data2"))), ==, "Relative-custom-value");
gtk_combo_box_set_active_id (GTK_COMBO_BOX (control (page, "shortcut-action")), "Run Command");
gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 1);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 4, &data, -1);
g_assert_cmpstr (data, ==, "Relative-custom-value"); g_free (data);
destroy_page (page); g_object_unref (store);
}
static void
test_shortcuts_duplicate_and_record (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
GtkListStore *store;
GtkWidget *page = shortcut_fixture (&store);
gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-duplicate")));
g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 4);
gtk_entry_set_text (GTK_ENTRY (control (page, "shortcut-data1")), "echo changed\\nnext");
GtkTreeIter iter; char *data;
gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 3);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
g_assert_cmpstr (data, ==, "echo changed\\nnext"); g_free (data);
gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-record")));
GtkWidget *dialog = NULL;
GList *tops = gtk_window_list_toplevels ();
for (GList *i = tops; i; i = i->next) if (!strcmp (gtk_widget_get_name (i->data), "shortcut-recorder")) dialog = i->data;
g_list_free (tops); g_assert_nonnull (dialog);
GtkWidget *view = control (dialog, "GtkTreeView");
GList *cells = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (gtk_tree_view_get_column (GTK_TREE_VIEW (view), 0)));
g_signal_emit_by_name (cells->data, "accel-edited", "0", GDK_KEY_Tab, GDK_SHIFT_MASK | GDK_LOCK_MASK, 23);
g_list_free (cells);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 1, &data, -1);
guint key; GdkModifierType mods;
gtk_accelerator_parse (data, &key, &mods);
g_assert_cmpuint (key, ==, GDK_KEY_ISO_Left_Tab);
g_assert_cmpuint (mods, ==, GDK_SHIFT_MASK); g_free (data);
gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-remove")));
g_assert_cmpint (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL), ==, 3);
destroy_page (page); g_object_unref (store);
}
static void
test_shortcuts_order_and_recorder_close (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
GtkListStore *store;
GtkWidget *page = shortcut_fixture (&store);
GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_object_ref_sink (window);
gtk_container_add (GTK_CONTAINER (window), page);
gtk_widget_show_all (window);
GtkTreePath *path = gtk_tree_path_new_from_indices (2, -1);
preferences_shortcuts_reveal_path (page, path); gtk_tree_path_free (path);
GdkEventKey event = {0};
event.type = GDK_KEY_PRESS; event.keyval = GDK_KEY_Up; event.state = GDK_SHIFT_MASK;
gboolean handled = FALSE;
g_signal_emit_by_name (control (page, "shortcuts-list"), "key-press-event", &event, &handled);
g_assert_true (handled);
GtkTreeIter iter; char *data;
gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 1);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
g_assert_cmpstr (data, ==, "echo original\\nsecond"); g_free (data);
g_signal_emit_by_name (control (page, "shortcuts-list"), "key-press-event", &event, &handled);
gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 3, &data, -1);
g_assert_cmpstr (data, ==, "\002"); g_free (data);
gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-record")));
GList *tops = gtk_window_list_toplevels ();
GtkWidget *dialog = NULL;
for (GList *i = tops; i; i = i->next) if (!strcmp (gtk_widget_get_name (i->data), "shortcut-recorder")) dialog = i->data;
g_list_free (tops); g_assert_nonnull (dialog);
gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (store), &iter, NULL, 1);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, 1, &data, -1);
g_assert_cmpstr (data, ==, "<Control>k"); g_free (data);
gtk_button_clicked (GTK_BUTTON (control (page, "shortcut-record")));
gtk_widget_destroy (window);
g_object_unref (window); g_object_unref (store);
}
int main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
gtk_available = gtk_init_check (&argc, &argv);
g_test_add_func ("/preferences/pages/open-preserves-values", test_open_preserves_values);
g_test_add_func ("/preferences/pages/timestamps-and-dependencies", test_timestamp_and_dependencies);
g_test_add_func ("/preferences/pages/conversation-preview", test_preview_updates);
g_test_add_func ("/preferences/pages/layout-switching", test_layout_switching);
g_test_add_func ("/preferences/pages/notification-mapping", test_notification_mapping);
g_test_add_func ("/preferences/pages/shared-sound", test_shared_sound_and_effects);
g_test_add_func ("/preferences/pages/notification-test", test_notification_test_safety);
g_test_add_func ("/preferences/history/open-preserves-values", test_history_preserves_values);
g_test_add_func ("/preferences/history/presets-and-custom", test_history_presets_and_custom);
g_test_add_func ("/preferences/history/dependencies-and-timestamps", test_history_dependencies_and_timestamps);
g_test_add_func ("/preferences/history/validation", test_history_validation);
g_test_add_func ("/preferences/typing/languages-preserved", test_typing_preservation_and_languages);
g_test_add_func ("/preferences/typing/completion-validation", test_typing_completion_and_validation);
g_test_add_func ("/preferences/shortcuts/filter-and-protection", test_shortcuts_filter_and_protection);
g_test_add_func ("/preferences/shortcuts/duplicate-and-record", test_shortcuts_duplicate_and_record);
g_test_add_func ("/preferences/shortcuts/order-and-recorder-close", test_shortcuts_order_and_recorder_close);
int result = g_test_run ();
g_free (played_file); g_free (opened_page);
g_free (opened_folder); g_free (preview_mask);
return result;
}
@@ -0,0 +1,226 @@
/* ZoiteChat
* Copyright (C) 1998-2010 Peter Zelezny.
* Copyright (C) 2009-2013 Berke Viktor.
* Copyright (C) 2026 deepend-tildeclub.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
/* Regression coverage for saving multiple Preferences editors together. */
#include "../preferences-persistence.h"
#include "../../common/cfgfiles.h"
#include "../theme/theme-runtime.h"
#include <errno.h>
#include <glib/gstdio.h>
#include <string.h>
static char *test_directory;
static gboolean fail_theme_prepare;
static int rename_count, fail_install, fail_restore;
char *get_xdir (void) { return test_directory; }
static gboolean
prepare (const char *name, const char *contents, char **path)
{
*path = g_build_filename (test_directory, name, NULL);
return g_file_set_contents (*path, contents, -1, NULL);
}
int save_config_prepare (char **path) { return prepare ("config.new", "new config", path); }
gboolean theme_runtime_save_prepare (char **path)
{
if (fail_theme_prepare) return FALSE;
return prepare ("theme.new", "new theme", path);
}
static int
checked_rename (const char *source, const char *destination)
{
rename_count++;
if (rename_count == fail_install || rename_count == fail_restore)
{
errno = EACCES;
return -1;
}
return g_rename (source, destination);
}
/* Exercise replacement failures without relying on filesystem permissions. */
#undef g_rename
#define g_rename checked_rename
#include "../preferences-persistence.c"
#undef g_rename
static void
put (const char *name, const char *contents)
{
char *path = g_build_filename (test_directory, name, NULL);
g_assert_true (g_file_set_contents (path, contents, -1, NULL));
g_free (path);
}
static void
assert_contents (const char *name, const char *expected)
{
char *path = g_build_filename (test_directory, name, NULL), *contents = NULL;
g_assert_true (g_file_get_contents (path, &contents, NULL, NULL));
g_assert_cmpstr (contents, ==, expected);
g_free (contents);
g_free (path);
}
static void
begin (void)
{
test_directory = g_dir_make_tmp ("zoitechat-preferences-XXXXXX", NULL);
g_assert_nonnull (test_directory);
rename_count = fail_install = fail_restore = 0;
fail_theme_prepare = FALSE;
put ("zoitechat.conf", "old config");
put ("colors.conf", "old theme");
put ("commands.conf", "old commands");
preferences_persistence_stage_file ("commands.conf", "new commands");
preferences_persistence_stage_file ("replace.conf", "new replacements");
}
static int
temporary_count (void)
{
GDir *directory = g_dir_open (test_directory, 0, NULL);
const char *name;
int count = 0;
while ((name = g_dir_read_name (directory)))
if (g_str_has_prefix (name, ".preferences-")) count++;
g_dir_close (directory);
return count;
}
static void
end (void)
{
GDir *directory = g_dir_open (test_directory, 0, NULL);
const char *name;
preferences_persistence_clear_staged_files ();
while ((name = g_dir_read_name (directory)))
{
char *path = g_build_filename (test_directory, name, NULL);
if (g_file_test (path, G_FILE_TEST_IS_DIR)) g_rmdir (path);
else g_unlink (path);
g_free (path);
}
g_dir_close (directory);
g_rmdir (test_directory);
g_free (test_directory);
}
static void
test_success (void)
{
PreferencesPersistenceResult result;
begin ();
result = preferences_persistence_save_all ();
g_assert_true (result.success);
assert_contents ("zoitechat.conf", "new config");
assert_contents ("colors.conf", "new theme");
assert_contents ("commands.conf", "new commands");
assert_contents ("replace.conf", "new replacements");
g_assert_cmpint (temporary_count (), ==, 0);
end ();
}
static void
test_prepare_failure (void)
{
PreferencesPersistenceResult result;
begin ();
fail_theme_prepare = TRUE;
result = preferences_persistence_save_all ();
g_assert_false (result.success);
g_assert_false (result.partial_failure);
g_assert_true (result.theme_failed);
assert_contents ("zoitechat.conf", "old config");
assert_contents ("commands.conf", "old commands");
g_assert_cmpint (rename_count, ==, 0);
g_assert_cmpint (temporary_count (), ==, 0);
end ();
}
static void
test_backup_failure (void)
{
PreferencesPersistenceResult result;
char *path;
begin ();
path = g_build_filename (test_directory, "replace.conf", NULL);
g_assert_cmpint (g_mkdir (path, 0700), ==, 0);
g_free (path);
result = preferences_persistence_save_all ();
g_assert_false (result.success);
g_assert_cmpstr (result.failed_file, ==, "replace.conf");
assert_contents ("zoitechat.conf", "old config");
assert_contents ("commands.conf", "old commands");
g_assert_cmpint (rename_count, ==, 0);
g_assert_cmpint (temporary_count (), ==, 0);
end ();
}
static void
test_rollback_and_retry (void)
{
PreferencesPersistenceResult result;
begin ();
fail_install = 4;
result = preferences_persistence_save_all ();
g_assert_false (result.success);
g_assert_false (result.partial_failure);
g_assert_true (result.retry_possible);
assert_contents ("zoitechat.conf", "old config");
assert_contents ("colors.conf", "old theme");
assert_contents ("commands.conf", "old commands");
g_assert_cmpint (temporary_count (), ==, 0);
fail_install = 0;
result = preferences_persistence_save_all ();
g_assert_true (result.success);
assert_contents ("commands.conf", "new commands");
end ();
}
static void
test_recovery_copy (void)
{
PreferencesPersistenceResult result;
begin ();
fail_install = 4;
fail_restore = 5;
result = preferences_persistence_save_all ();
g_assert_false (result.success);
g_assert_true (result.partial_failure);
g_assert_cmpint (temporary_count (), ==, 1);
assert_contents ("zoitechat.conf", "old config");
assert_contents ("colors.conf", "old theme");
end ();
}
int main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/preferences/save/success", test_success);
g_test_add_func ("/preferences/save/prepare-failure", test_prepare_failure);
g_test_add_func ("/preferences/save/backup-failure", test_backup_failure);
g_test_add_func ("/preferences/save/rollback-retry", test_rollback_and_retry);
g_test_add_func ("/preferences/save/recovery-copy", test_recovery_copy);
return g_test_run ();
}
+180
View File
@@ -0,0 +1,180 @@
/* Spell-entry dictionary reload regressions. GPL-2.0-or-later. */
#include "../../common/zoitechat.h"
/* Exercise the actual widget with a deterministic dictionary provider. This
* must also run on builders without Enchant or installed dictionaries. */
#include "../sexy-spell-entry.c"
struct zoitechatprefs prefs;
struct EnchantBroker { int unused; };
struct EnchantDict { char *language; };
static gboolean gtk_available, dictionaries_available;
static int active_dicts, checked_words;
gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *color)
{
return gdk_rgba_parse (color, token == THEME_TOKEN_TEXT_BACKGROUND ? "#ffffff" : "#000000");
}
gboolean theme_get_mirc_color (unsigned int index, GdkRGBA *color)
{
return gdk_rgba_parse (color, "#000000");
}
void theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *values)
{
memset (values, 0, sizeof *values);
gdk_rgba_parse (&values->background, "#ffffff");
}
void theme_palette_color_get_rgb16 (const GdkRGBA *color, guint16 *red, guint16 *green, guint16 *blue)
{
*red = color->red * 65535;
*green = color->green * 65535;
*blue = color->blue * 65535;
}
const char *gtkutil_icon_name_from_stock (const char *stock) { return stock; }
GtkWidget *gtkutil_image_new_from_stock (const char *stock, GtkIconSize size)
{
return gtk_image_new_from_icon_name (stock, size);
}
#ifdef HAVE_ISO_CODES
void codetable_init (void) { }
void codetable_free (void) { }
void codetable_lookup (const gchar *code, const gchar **language, const gchar **country)
{
*language = code;
*country = "";
}
#endif
static struct EnchantBroker *test_broker_new (void) { return g_new0 (struct EnchantBroker, 1); }
static void test_broker_free (struct EnchantBroker *broker) { g_free (broker); }
static void test_list_dicts (struct EnchantBroker *broker, EnchantDictDescribeFn describe, void *data)
{
if (dictionaries_available) describe ("en_US", "test", "test", "", data);
}
static struct EnchantDict *test_request_dict (struct EnchantBroker *broker, const char *const language)
{
if (!dictionaries_available || (strcmp (language, "en_US") && strcmp (language, "en"))) return NULL;
struct EnchantDict *dict = g_new0 (struct EnchantDict, 1);
dict->language = g_strdup (language);
active_dicts++;
return dict;
}
static void test_free_dict (struct EnchantBroker *broker, struct EnchantDict *dict)
{
g_free (dict->language);
g_free (dict);
active_dicts--;
}
static void test_describe_dict (struct EnchantDict *dict, EnchantDictDescribeFn describe, void *data)
{
describe (dict->language, "test", "test", "", data);
}
static void test_add_word (struct EnchantDict *dict, const char *const word, ssize_t length) { }
static int test_check_word (struct EnchantDict *dict, const char *const word, ssize_t length)
{
checked_words++;
return 0;
}
static SexySpellEntry *
new_input (gboolean available)
{
dictionaries_available = available;
memset (&prefs, 0, sizeof prefs);
g_strlcpy (prefs.hex_text_spell_langs, "missing_XX", sizeof prefs.hex_text_spell_langs);
SexySpellEntry *entry = SEXY_SPELL_ENTRY (sexy_spell_entry_new ());
g_object_ref_sink (entry);
return entry;
}
static void
destroy_input (SexySpellEntry *entry)
{
gtk_widget_destroy (GTK_WIDGET (entry));
g_object_unref (entry);
g_assert_cmpint (active_dicts, ==, 0);
}
static void
test_first_dictionary_on_empty_input (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
SexySpellEntry *entry = new_input (FALSE);
g_assert_null (entry->priv->dict_list);
/* The real Save path: an untouched input, no old dictionary, then a newly
* selected dictionary. Before the fix, the final recheck dereferenced NULL. */
dictionaries_available = TRUE;
g_strlcpy (prefs.hex_text_spell_langs, "en_US", sizeof prefs.hex_text_spell_langs);
for (int i = 0; i < 3; i++)
{
sexy_spell_entry_deactivate_language (entry, NULL);
sexy_spell_entry_activate_default_languages (entry);
g_assert_true (sexy_spell_entry_language_is_active (entry, "en_US"));
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, "");
}
destroy_input (entry);
}
static void
test_reload_preserves_draft (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
SexySpellEntry *entry = new_input (FALSE);
const char *draft = "Hello café";
gtk_entry_set_text (GTK_ENTRY (entry), draft);
dictionaries_available = TRUE;
g_strlcpy (prefs.hex_text_spell_langs, "en_US", sizeof prefs.hex_text_spell_langs);
checked_words = 0;
sexy_spell_entry_deactivate_language (entry, NULL);
sexy_spell_entry_activate_default_languages (entry);
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, draft);
g_assert_cmpint (checked_words, >, 0);
/* Save-failure rollback and retry also reload the input dictionaries. */
dictionaries_available = FALSE;
sexy_spell_entry_deactivate_language (entry, NULL);
sexy_spell_entry_activate_default_languages (entry);
dictionaries_available = TRUE;
sexy_spell_entry_activate_default_languages (entry);
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (entry)), ==, draft);
destroy_input (entry);
}
static void
test_empty_input_spelling_toggles (void)
{
if (!gtk_available) { g_test_skip ("GTK display unavailable"); return; }
SexySpellEntry *entry = new_input (TRUE);
sexy_spell_entry_set_parse_attributes (entry, FALSE);
sexy_spell_entry_set_checked (entry, FALSE);
sexy_spell_entry_set_checked (entry, TRUE);
sexy_spell_entry_set_parse_attributes (entry, TRUE);
sexy_spell_entry_deactivate_language (entry, NULL);
sexy_spell_entry_activate_default_languages (entry);
g_assert_true (sexy_spell_entry_language_is_active (entry, "en"));
destroy_input (entry);
}
int main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
gtk_available = gtk_init_check (&argc, &argv);
if (gtk_available)
{
SexySpellEntryClass *klass = g_type_class_ref (SEXY_TYPE_SPELL_ENTRY);
have_enchant = TRUE;
klass->word_check = default_word_check;
enchant_broker_init = test_broker_new;
enchant_broker_free = test_broker_free;
enchant_broker_list_dicts = test_list_dicts;
enchant_broker_request_dict = test_request_dict;
enchant_broker_free_dict = test_free_dict;
enchant_dict_describe = test_describe_dict;
enchant_dict_add_to_session = test_add_word;
enchant_dict_check = test_check_word;
}
g_test_add_func ("/spell-entry/first-dictionary-empty-input", test_first_dictionary_on_empty_input);
g_test_add_func ("/spell-entry/reload-preserves-draft", test_reload_preserves_draft);
g_test_add_func ("/spell-entry/empty-input-spelling-toggles", test_empty_input_spelling_toggles);
return g_test_run ();
}
+568
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;
@@ -210,6 +211,70 @@ static void test_formatting (Fixture *f, gconstpointer data)
"nick a\002bold\002 \035italic\035 \00304,02red\017 \037u\037 \036s\036 \026r\026"); "nick a\002bold\002 \035italic\035 \00304,02red\017 \037u\037 \036s\036 \026r\026");
} }
static void
assert_same_xtext_pixels (cairo_surface_t *a, cairo_surface_t *b)
{
g_assert_cmpint (cairo_image_surface_get_width (a), ==, cairo_image_surface_get_width (b));
g_assert_cmpint (cairo_image_surface_get_height (a), ==, cairo_image_surface_get_height (b));
g_assert_cmpint (cairo_image_surface_get_stride (a), ==, cairo_image_surface_get_stride (b));
g_assert_cmpint (memcmp (cairo_image_surface_get_data (a), cairo_image_surface_get_data (b),
cairo_image_surface_get_stride (a) * cairo_image_surface_get_height (a)), ==, 0);
}
static void
test_timestamp_indent_toggle (Fixture *f, gconstpointer data)
{
xtext_buffer *buf = f->xtext->buffer;
cairo_surface_t *before, *after, *hidden;
char *selection;
int length;
/* Previously indented entries must retain their margin timestamps. */
gtk_xtext_set_indent (f->xtext, TRUE);
gtk_xtext_set_time_stamp (buf, TRUE);
buf->indent = 130;
gtk_xtext_append_indent (buf, (guchar *)"nick", -1, (guchar *)"hello", -1, 1234);
gtk_xtext_recalc_widths (buf, FALSE);
before = snapshot (f, FALSE);
gtk_xtext_set_indent (f->xtext, FALSE);
after = snapshot (f, FALSE);
assert_same_xtext_pixels (before, after);
/* Check that a visible stamp, not just an unchanged blank margin,
* was preserved. */
gtk_xtext_set_time_stamp (buf, FALSE);
hidden = snapshot (f, FALSE);
g_assert_cmpint (memcmp (cairo_image_surface_get_data (after),
cairo_image_surface_get_data (hidden),
cairo_image_surface_get_stride (after) * cairo_image_surface_get_height (after)), !=, 0);
cairo_surface_destroy (hidden);
cairo_surface_destroy (after);
cairo_surface_destroy (before);
/* Entries already containing an inline stamp must not gain a second
* margin stamp after enabling indentation. */
gtk_xtext_clear (buf, 0);
gtk_xtext_set_time_stamp (buf, TRUE);
gtk_xtext_append_wrap (buf, (guchar *)"[12:34] nick hello", -1, 1234, 8);
before = snapshot (f, FALSE);
gtk_xtext_set_indent (f->xtext, TRUE);
after = snapshot (f, FALSE);
assert_same_xtext_pixels (before, after);
cairo_surface_destroy (after);
cairo_surface_destroy (before);
/* The same inline stamp must not be duplicated when copied. */
buf->text_last->mark_start = 0;
buf->text_last->mark_end = buf->text_last->str_len;
buf->last_ent_start = buf->text_last;
buf->last_ent_end = buf->text_last;
f->xtext->selection_buffer = buf;
f->xtext->mark_stamp = TRUE;
selection = gtk_xtext_selection_get_text (f->xtext, &length);
g_assert_cmpstr (selection, ==, "[12:34] nick hello");
g_free (selection);
}
static void select_entries (Fixture *f, textentry *start, textentry *end) static void select_entries (Fixture *f, textentry *start, textentry *end)
{ {
textentry *ent; textentry *ent;
@@ -252,6 +317,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 +1096,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);
@@ -576,7 +1142,9 @@ int main (int argc, char **argv)
ADD ("layout/font-isolation", test_font_isolation); ADD ("layout/font-isolation", test_font_isolation);
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 ("render/timestamps-after-indent-toggle", test_timestamp_indent_toggle);
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);
+247 -56
View File
@@ -38,6 +38,8 @@
#include "theme/theme-access.h" #include "theme/theme-access.h"
#include "theme/theme-manager.h" #include "theme/theme-manager.h"
#include "textgui.h" #include "textgui.h"
#include "setup.h"
#include "preferences-persistence.h"
#define ICON_TEXTEVENT_SAVE_AS "document-save-as" #define ICON_TEXTEVENT_SAVE_AS "document-save-as"
#define ICON_TEXTEVENT_OPEN "document-open" #define ICON_TEXTEVENT_OPEN "document-open"
@@ -50,6 +52,10 @@ extern char *pntevts[];
static GtkWidget *pevent_dialog = NULL, *pevent_dialog_twid, static GtkWidget *pevent_dialog = NULL, *pevent_dialog_twid,
*pevent_dialog_list, *pevent_dialog_hlist; *pevent_dialog_list, *pevent_dialog_hlist;
static char *pevent_staged[NUM_XP];
static char *pevent_original[NUM_XP];
static gboolean pevent_pending;
#define PEVENT_THEME_LISTENER_ID_KEY "textgui.theme-listener-id" #define PEVENT_THEME_LISTENER_ID_KEY "textgui.theme-listener-id"
enum enum
@@ -148,10 +154,17 @@ PrintTextRaw (void *xtbuf, unsigned char *text, int indent, time_t stamp)
} }
static void static void
pevent_dialog_close (GtkWidget *wid, gpointer arg) pevent_preferences_destroy (GtkWidget *widget, gpointer data)
{ {
pevent_dialog = NULL; int i;
pevent_save (NULL); (void)widget; (void)data;
pevent_dialog = pevent_dialog_list = pevent_dialog_hlist = pevent_dialog_twid = NULL;
for (i = 0; i < NUM_XP; i++)
{
g_clear_pointer (&pevent_staged[i], g_free);
g_clear_pointer (&pevent_original[i], g_free);
}
pevent_pending = FALSE;
} }
static void static void
@@ -211,9 +224,14 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo
char *out; char *out;
int sig; int sig;
if (!gtkutil_treeview_get_selected (GTK_TREE_VIEW (pevent_dialog_list), path = gtk_tree_path_new_from_string (pathstr);
&iter, ROW_COLUMN, &sig, -1)) if (!gtk_tree_model_get_iter (model, &iter, path))
{
gtk_tree_path_free (path);
return; return;
}
gtk_tree_path_free (path);
gtk_tree_model_get (model, &iter, ROW_COLUMN, &sig, -1);
text = new_text; text = new_text;
len = strlen (new_text); len = strlen (new_text);
@@ -234,16 +252,11 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo
return; return;
} }
path = gtk_tree_path_new_from_string (pathstr);
gtk_tree_model_get_iter (model, &iter, path);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, TEXT_COLUMN, new_text, -1); gtk_list_store_set (GTK_LIST_STORE (model), &iter, TEXT_COLUMN, new_text, -1);
gtk_tree_path_free (path);
g_free (pntevts_text[sig]); g_free (pevent_staged[sig]);
g_free (pntevts[sig]); pevent_staged[sig] = g_strdup (text);
g_free (out);
pntevts_text[sig] = g_strdup (text);
pntevts[sig] = out;
out = g_malloc (len + 2); out = g_malloc (len + 2);
memcpy (out, text, len + 1); memcpy (out, text, len + 1);
@@ -257,8 +270,7 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo
/* Scroll to bottom */ /* Scroll to bottom */
gtk_adjustment_set_value (xtext->adj, gtk_adjustment_get_upper (xtext->adj)); gtk_adjustment_set_value (xtext->adj, gtk_adjustment_get_upper (xtext->adj));
/* save this when we exit */ /* The Preferences Save action commits this staged template. */
prefs.save_pevents = 1;
} }
static void static void
@@ -316,53 +328,222 @@ pevent_dialog_fill (GtkWidget *list)
i--; i--;
gtk_list_store_insert_with_values (store, &iter, 0, gtk_list_store_insert_with_values (store, &iter, 0,
EVENT_COLUMN, te[i].name, EVENT_COLUMN, te[i].name,
TEXT_COLUMN, pntevts_text[i], TEXT_COLUMN, pevent_staged[i],
ROW_COLUMN, i, -1); ROW_COLUMN, i, -1);
} }
while (i != 0); while (i != 0);
} }
static void static char *
pevent_save_req_cb (void *arg1, char *file) pevent_preferences_serialize (void)
{ {
if (file) GString *text = g_string_new ("");
pevent_save (file); int i;
for (i = 0; i < NUM_XP; i++)
g_string_append_printf (text, "event_name=%s\nevent_text=%s\n\n", te[i].name, pevent_staged[i]);
return g_string_free (text, FALSE);
} }
static void static gboolean
pevent_save_cb (GtkWidget * wid, void *data) pevent_preferences_validate (const char *text, int event, GError **error)
{ {
if (data) char *compiled = NULL;
int maximum;
if (strpbrk (text, "\r\n") || pevt_build_string (text, &compiled, &maximum) != 0)
{ {
gtkutil_file_req (NULL, _("Print Texts File"), pevent_save_req_cb, NULL, g_free (compiled);
NULL, NULL, FRF_WRITE); g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Invalid message format for %s."), te[event].name);
return; return FALSE;
} }
pevent_save (NULL); g_free (compiled);
if (maximum > (te[event].num_args & 0x7f))
{
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("%s only supports %d arguments."), te[event].name, te[event].num_args & 0x7f);
return FALSE;
}
return TRUE;
} }
static void gboolean
pevent_load_req_cb (void *arg1, char *file) pevent_preferences_import_text (const char *contents, GError **error)
{ {
if (file) char **lines;
char *values[NUM_XP] = {0};
int i, event = -1, matched = 0;
gboolean success = FALSE;
if (!pevent_dialog_list)
{ {
pevent_load (file); g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Open message formatting before importing templates."));
pevent_make_pntevts (); return FALSE;
}
lines = g_strsplit (contents, "\n", -1);
for (i = 0; lines[i]; i++)
{
char *line = lines[i];
gsize length = strlen (line);
int n;
if (length && line[length - 1] == '\r') line[length - 1] = 0;
if (g_str_has_prefix (line, "event_name="))
{
event = -1;
for (n = 0; n < NUM_XP; n++)
if (!strcmp (te[n].name, line + 11)) { event = n; break; }
}
else if (event >= 0 && g_str_has_prefix (line, "event_text="))
{
if (!pevent_preferences_validate (line + 11, event, error)) goto done;
g_free (values[event]);
values[event] = g_strdup (line + 11);
matched++;
}
}
if (!matched)
{
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("No recognized text events were found in this file."));
goto done;
}
for (i = 0; i < NUM_XP; i++)
if (values[i])
{
g_free (pevent_staged[i]);
pevent_staged[i] = values[i];
values[i] = NULL;
}
pevent_dialog_fill (pevent_dialog_list); pevent_dialog_fill (pevent_dialog_list);
prefs.save_pevents = 1; success = TRUE;
done:
for (i = 0; i < NUM_XP; i++) g_free (values[i]);
g_strfreev (lines);
return success;
}
static gboolean
pevent_preferences_import (const char *file, GError **error)
{
char *contents;
if (!g_file_get_contents (file, &contents, NULL, error)) return FALSE;
gboolean result = pevent_preferences_import_text (contents, error);
g_free (contents);
return result;
}
typedef struct
{
GWeakRef page;
gboolean exporting;
} PeEventFileRequest;
static void
pevent_file_request_free (gpointer data)
{
PeEventFileRequest *request = data;
g_weak_ref_clear (&request->page);
g_free (request);
}
static void
pevent_file_response (GtkNativeDialog *dialog, gint response, PeEventFileRequest *request)
{
GtkWidget *page = g_weak_ref_get (&request->page);
if (page && page == pevent_dialog && response == GTK_RESPONSE_ACCEPT)
{
char *file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
GError *error = NULL;
if (file)
{
if (request->exporting)
{
char *contents = pevent_preferences_serialize ();
g_file_set_contents (file, contents, -1, &error);
g_free (contents);
} }
else pevent_preferences_import (file, &error);
if (error) { fe_message (error->message, FE_MSG_ERROR); g_clear_error (&error); }
g_free (file);
}
}
if (page) g_object_unref (page);
g_object_unref (dialog);
} }
static void static void
pevent_load_cb (GtkWidget * wid, void *data) pevent_file_open (GtkWidget *button, gpointer exporting)
{ {
gtkutil_file_req (NULL, _("Print Texts File"), pevent_load_req_cb, NULL, NULL, NULL, 0); GtkFileChooserNative *dialog;
PeEventFileRequest *request = g_new0 (PeEventFileRequest, 1);
(void)button;
request->exporting = GPOINTER_TO_INT (exporting);
g_weak_ref_init (&request->page, pevent_dialog);
dialog = gtk_file_chooser_native_new (_("Text events file"),
GTK_WINDOW (gtk_widget_get_toplevel (pevent_dialog)),
request->exporting ? GTK_FILE_CHOOSER_ACTION_SAVE : GTK_FILE_CHOOSER_ACTION_OPEN,
request->exporting ? _("_Export") : _("_Import"), _("_Cancel"));
gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (dialog), TRUE);
g_object_set_data_full (G_OBJECT (dialog), "request", request, pevent_file_request_free);
g_signal_connect (dialog, "response", G_CALLBACK (pevent_file_response), request);
gtk_native_dialog_show (GTK_NATIVE_DIALOG (dialog));
} }
static void gboolean
pevent_ok_cb (GtkWidget * wid, void *data) pevent_preferences_stage (GError **error)
{ {
gtk_widget_destroy (pevent_dialog); int i;
char *contents;
pevent_pending = FALSE;
if (!pevent_dialog) return TRUE;
for (i = 0; i < NUM_XP; i++)
{
if (!pevent_preferences_validate (pevent_staged[i], i, error))
{
GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (pevent_dialog_list));
GtkTreeIter iter;
gboolean valid = gtk_tree_model_get_iter_first (model, &iter);
setup_reveal_widget (pevent_dialog_list);
while (valid)
{
int event;
gtk_tree_model_get (model, &iter, ROW_COLUMN, &event, -1);
if (event == i)
{
GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
gtk_widget_grab_focus (pevent_dialog_list);
gtk_tree_view_set_cursor (GTK_TREE_VIEW (pevent_dialog_list), path, NULL, FALSE);
gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (pevent_dialog_list), path, NULL, FALSE, 0, 0);
gtk_tree_path_free (path);
break;
}
valid = gtk_tree_model_iter_next (model, &iter);
}
return FALSE;
}
if (g_strcmp0 (pevent_staged[i], pevent_original[i]) != 0) pevent_pending = TRUE;
}
if (!pevent_pending) return TRUE;
for (i = 0; i < NUM_XP; i++)
if (g_strcmp0 (pntevts_text[i], pevent_original[i]) != 0)
{
setup_reveal_widget (pevent_dialog_list);
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_AGAIN, _("Text events changed outside Preferences. Reopen Preferences before editing them."));
return FALSE;
}
contents = pevent_preferences_serialize ();
preferences_persistence_stage_file ("pevents.conf", contents);
g_free (contents);
return TRUE;
}
void
pevent_preferences_commit (void)
{
int i;
if (!pevent_pending) return;
for (i = 0; i < NUM_XP; i++)
{
g_free (pntevts_text[i]);
pntevts_text[i] = g_strdup (pevent_staged[i]);
}
pevent_make_pntevts ();
prefs.save_pevents = 1;
} }
static void static void
@@ -373,7 +554,7 @@ pevent_test_cb (GtkWidget * wid, GtkWidget * twid)
for (n = 0; n < NUM_XP; n++) for (n = 0; n < NUM_XP; n++)
{ {
text = _(pntevts_text[n]); text = pevent_staged[n];
len = strlen (text); len = strlen (text);
out = g_malloc (len + 2); out = g_malloc (len + 2);
@@ -485,22 +666,27 @@ pevent_hlist_treeview_new (GtkWidget *box)
return view; return view;
} }
void static void
pevent_dialog_show (void) pevent_preferences_preview_realize (GtkWidget *widget, gpointer data)
{
gtk_xtext_set_font (GTK_XTEXT (widget), prefs.hex_text_font);
}
GtkWidget *
pevent_preferences_create (void)
{ {
GtkWidget *vbox, *hbox, *wid, *pane, *preview_pane; GtkWidget *vbox, *hbox, *wid, *pane, *preview_pane;
XTextColor xtext_palette[XTEXT_COLS]; XTextColor xtext_palette[XTEXT_COLS];
if (pevent_dialog) int i;
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
pevent_dialog = vbox;
for (i = 0; i < NUM_XP; i++)
{ {
mg_bring_tofront (pevent_dialog); pevent_original[i] = g_strdup (pntevts_text[i]);
return; pevent_staged[i] = g_strdup (pntevts_text[i]);
} }
g_signal_connect (vbox, "destroy", G_CALLBACK (pevent_preferences_destroy), NULL);
pevent_dialog =
mg_create_generic_tab ("edit events", _("Edit Events"),
TRUE, FALSE, pevent_dialog_close, NULL,
600, 455, &vbox, 0);
preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL); preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
gtk_box_pack_start (GTK_BOX (vbox), preview_pane, TRUE, TRUE, 0); gtk_box_pack_start (GTK_BOX (vbox), preview_pane, TRUE, TRUE, 0);
@@ -523,7 +709,7 @@ pevent_dialog_show (void)
pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0); pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0);
gtk_widget_set_sensitive (pevent_dialog_twid, FALSE); gtk_widget_set_sensitive (pevent_dialog_twid, FALSE);
gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid); gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid);
gtk_xtext_set_font (GTK_XTEXT (pevent_dialog_twid), prefs.hex_text_font); g_signal_connect (pevent_dialog_twid, "realize", G_CALLBACK (pevent_preferences_preview_realize), NULL);
g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid); g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid);
g_object_set_data (G_OBJECT (pevent_dialog), PEVENT_THEME_LISTENER_ID_KEY, g_object_set_data (G_OBJECT (pevent_dialog), PEVENT_THEME_LISTENER_ID_KEY,
GUINT_TO_POINTER (theme_listener_register ("textgui.events", pevent_dialog_theme_changed, pevent_dialog))); GUINT_TO_POINTER (theme_listener_register ("textgui.events", pevent_dialog_theme_changed, pevent_dialog)));
@@ -532,15 +718,20 @@ pevent_dialog_show (void)
hbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); hbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
gtk_button_box_set_layout (GTK_BUTTON_BOX (hbox), GTK_BUTTONBOX_SPREAD); gtk_button_box_set_layout (GTK_BUTTON_BOX (hbox), GTK_BUTTONBOX_SPREAD);
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 4); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 4);
gtkutil_button (hbox, ICON_TEXTEVENT_SAVE_AS, NULL, pevent_save_cb, gtkutil_button (hbox, ICON_TEXTEVENT_SAVE_AS, NULL, pevent_file_open,
(void *) 1, _("Save As...")); (void *) 1, _("Export..."));
gtkutil_button (hbox, ICON_TEXTEVENT_OPEN, NULL, pevent_load_cb, gtkutil_button (hbox, ICON_TEXTEVENT_OPEN, NULL, pevent_file_open,
NULL, _("Load From...")); NULL, _("Import..."));
gtkutil_button (hbox, NULL, NULL, pevent_test_cb, gtkutil_button (hbox, NULL, NULL, pevent_test_cb,
pevent_dialog_twid, _("Test All")); pevent_dialog_twid, _("Test All"));
gtkutil_button (hbox, ICON_TEXTEVENT_OK, NULL, pevent_ok_cb,
NULL, _("OK"));
gtk_widget_show_all (pevent_dialog); gtk_widget_show_all (pevent_dialog);
pevent_dialog_theme_apply (pevent_dialog); pevent_dialog_theme_apply (pevent_dialog);
return vbox;
}
void
pevent_dialog_show (void)
{
setup_open_page ("events");
} }
+4
View File
@@ -22,5 +22,9 @@
void PrintTextRaw (void *xtbuf, unsigned char *text, int indent, time_t stamp); void PrintTextRaw (void *xtbuf, unsigned char *text, int indent, time_t stamp);
void pevent_dialog_show (void); void pevent_dialog_show (void);
GtkWidget *pevent_preferences_create (void);
gboolean pevent_preferences_import_text (const char *contents, GError **error);
gboolean pevent_preferences_stage (GError **error);
void pevent_preferences_commit (void);
#endif #endif
@@ -112,6 +112,14 @@ theme_runtime_mode_has_user_colors (gboolean dark_mode)
return FALSE; return FALSE;
} }
gboolean
theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color)
{
(void)map;
*color = stub_light_colors[token];
return TRUE;
}
gboolean gboolean
theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
{ {
@@ -571,6 +571,31 @@ test_stale_theme_symlink_is_replaced (void)
theme_gtk3_disable (); theme_gtk3_disable ();
} }
static void
test_builtin_appearances_restore_desktop_theme (void)
{
if (!gtk_available) return;
GtkSettings *settings = gtk_settings_get_default ();
char *original, *name;
gboolean dark;
theme_gtk3_disable ();
g_object_get (settings, "gtk-theme-name", &original, NULL);
g_strlcpy (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK, sizeof prefs.hex_gui_gtk3_theme);
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
g_assert_true (theme_gtk3_apply_current (NULL));
g_object_get (settings, "gtk-theme-name", &name, "gtk-application-prefer-dark-theme", &dark, NULL);
g_assert_cmpstr (name, ==, "Adwaita"); g_assert_true (dark); g_free (name);
g_strlcpy (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_LIGHT, sizeof prefs.hex_gui_gtk3_theme);
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_LIGHT;
g_assert_true (theme_gtk3_apply_current (NULL));
g_object_get (settings, "gtk-application-prefer-dark-theme", &dark, NULL);
g_assert_false (dark);
prefs.hex_gui_gtk3_theme[0] = '\0'; prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_AUTO;
g_assert_true (theme_gtk3_apply_current (NULL));
g_object_get (settings, "gtk-theme-name", &name, NULL);
g_assert_cmpstr (name, ==, original); g_free (name); g_free (original);
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
@@ -596,6 +621,7 @@ main (int argc, char **argv)
if (!gtk_available) if (!gtk_available)
g_test_message ("Skipping GTK3 settings tests because GTK initialization failed"); g_test_message ("Skipping GTK3 settings tests because GTK initialization failed");
g_test_add_func ("/theme/gtk3/builtin_appearances_restore_desktop", test_builtin_appearances_restore_desktop_theme);
rc = g_test_run (); rc = g_test_run ();
theme_gtk3_disable (); theme_gtk3_disable ();
teardown_themes (); teardown_themes ();
@@ -25,6 +25,7 @@
#include "../../../common/gtk3-theme-service.h" #include "../../../common/gtk3-theme-service.h"
#include "../theme-gtk3.h" #include "../theme-gtk3.h"
#include "../theme-manager.h" #include "../theme-manager.h"
#include "../theme-runtime.h"
struct session *current_sess; struct session *current_sess;
struct session *current_tab; struct session *current_tab;
@@ -38,6 +39,61 @@ static char applied_theme_id[256];
static ThemeGtk3Variant applied_variant; static ThemeGtk3Variant applied_variant;
static gboolean removed_selected; static gboolean removed_selected;
static gboolean custom_tokens[THEME_TOKEN_COUNT];
static GdkRGBA custom_colors[THEME_TOKEN_COUNT];
gboolean
theme_runtime_get_user_color (ThemeSemanticToken token, GdkRGBA *color)
{
if (!custom_tokens[token]) return FALSE;
*color = custom_colors[token];
return TRUE;
}
gboolean
theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color)
{
(void)map;
return gdk_rgba_parse (color, token == THEME_TOKEN_TEXT_BACKGROUND || token == THEME_TOKEN_SELECTION_FOREGROUND ? "#ffffff" :
token == THEME_TOKEN_SELECTION_BACKGROUND ? "#336699" : "#000000");
}
gboolean
theme_get_default_color_for_widget (GtkWidget *widget, ThemeSemanticToken token, GdkRGBA *color)
{
(void)widget;
theme_runtime_get_default_color (token, NULL, color);
if (token == THEME_TOKEN_TEXT_BACKGROUND && prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_DARK)
gdk_rgba_parse (color, "#181818");
return TRUE;
}
void theme_runtime_reset_user_color (ThemeSemanticToken token) { custom_tokens[token] = FALSE; }
void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *color)
{ custom_tokens[token] = TRUE; custom_colors[token] = *color; }
gboolean theme_manager_apply_mode (unsigned int mode, gboolean *changed)
{ (void)mode; if (changed) *changed = FALSE; return FALSE; }
void theme_manager_apply_and_dispatch (unsigned int mode, ThemeChangedReason reasons, gboolean *changed)
{ (void)mode; (void)reasons; if (changed) *changed = FALSE; }
guint theme_listener_register (const char *id, ThemeChangedCallback callback, gpointer data)
{ (void)id; (void)callback; (void)data; return 0; }
void theme_listener_unregister (guint id) { (void)id; }
gboolean pevent_preferences_import_text (const char *contents, GError **error)
{ (void)contents; (void)error; return TRUE; }
void
setup_open_page (const char *id)
{
(void)id;
}
void
setup_reveal_widget (GtkWidget *widget)
{
(void)widget;
}
GtkWidget * GtkWidget *
gtkutil_box_new (GtkOrientation orientation, gboolean homogeneous, gint spacing) gtkutil_box_new (GtkOrientation orientation, gboolean homogeneous, gint spacing)
{ {
@@ -106,6 +162,7 @@ theme_manager_set_token_color (unsigned int dark_mode, ThemeSemanticToken token,
(void)token; (void)token;
(void)color; (void)color;
set_token_color_calls++; set_token_color_calls++;
theme_runtime_user_set_color (token, color);
if (changed) if (changed)
*changed = FALSE; *changed = FALSE;
} }
@@ -458,6 +515,154 @@ test_select_theme_does_not_pin_palette_tokens (void)
gtk_widget_destroy (page); gtk_widget_destroy (page);
} }
static void
test_embedded_color_editor (void)
{
GtkWidget *window, *editor;
theme_color_manager_ui *ui;
theme_color_manager_row *row;
GError *error = NULL;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
memset (&prefs, 0, sizeof prefs);
theme_preferences_stage_begin ();
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
editor = theme_preferences_create_color_editor (GTK_WINDOW (window), NULL);
gtk_container_add (GTK_CONTAINER (window), editor);
ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager");
row = g_ptr_array_index (ui->rows, 0);
set_token_color_calls = 0;
theme_preferences_manager_entry_commit (row);
g_assert_true (theme_preferences_color_editor_stage (editor, &error));
g_assert_no_error (error);
theme_preferences_stage_apply ();
g_assert_cmpint (set_token_color_calls, ==, 0);
gtk_entry_set_text (GTK_ENTRY (row->entry), "#123456");
g_assert_true (theme_preferences_color_editor_stage (editor, &error));
g_assert_no_error (error);
/* A typed edit previews its token, then Save reapplies that token. */
g_assert_cmpint (set_token_color_calls, ==, 1);
theme_preferences_stage_apply ();
g_assert_cmpint (set_token_color_calls, ==, 2);
gtk_entry_set_text (GTK_ENTRY (row->entry), "invalid");
g_assert_false (theme_preferences_color_editor_stage (editor, &error));
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
g_clear_error (&error);
gtk_widget_show_all (window);
gtk_button_clicked (GTK_BUTTON (row->button));
gtk_widget_destroy (window);
theme_preferences_stage_discard ();
}
static void
test_appearance_schemes_and_cancel (void)
{
if (!gtk_available) { g_test_message ("GTK display not available"); return; }
memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens);
GdkRGBA original, restored;
gdk_rgba_parse (&original, "#345678");
theme_runtime_user_set_color (THEME_TOKEN_TEXT_FOREGROUND, &original);
theme_preferences_stage_begin ();
struct zoitechatprefs staged = prefs;
GtkWidget *page = theme_preferences_create_page (NULL, &staged, NULL);
theme_preferences_ui *appearance = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (appearance->color_editor), "zoitechat-theme-color-manager");
gtk_button_clicked (GTK_BUTTON (ui->scheme_buttons[2]));
g_assert_true (theme_preferences_stage.changed);
g_assert_true (theme_preferences_stage.staged_custom[THEME_TOKEN_TEXT_BACKGROUND]);
gtk_button_clicked (GTK_BUTTON (appearance->style_buttons[2]));
g_assert_cmpint (prefs.hex_gui_dark_mode, ==, ZOITECHAT_DARK_MODE_DARK);
theme_preferences_stage_discard ();
g_assert_cmpint (prefs.hex_gui_dark_mode, ==, ZOITECHAT_DARK_MODE_AUTO);
g_assert_true (theme_runtime_get_user_color (THEME_TOKEN_TEXT_FOREGROUND, &restored));
g_assert_true (gdk_rgba_equal (&original, &restored));
for (int token = 0; token < THEME_TOKEN_COUNT; token++)
if (token != THEME_TOKEN_TEXT_FOREGROUND) g_assert_false (custom_tokens[token]);
gtk_widget_destroy (page);
}
static void
test_individual_reset_preserves_other_overrides (void)
{
if (!gtk_available) { g_test_message ("GTK display not available"); return; }
memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens);
theme_preferences_stage_begin ();
GtkWidget *editor = theme_preferences_create_color_editor (NULL, NULL);
theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager");
gtk_button_clicked (GTK_BUTTON (ui->scheme_buttons[2]));
theme_color_manager_row *row = g_ptr_array_index (ui->rows, THEME_TOKEN_TEXT_BACKGROUND);
gtk_button_clicked (GTK_BUTTON (row->reset));
g_assert_false (theme_preferences_stage.staged_custom[THEME_TOKEN_TEXT_BACKGROUND]);
g_assert_true (theme_preferences_stage.staged_custom[THEME_TOKEN_TEXT_FOREGROUND]);
g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (row->status)), ==, "Theme");
GdkRGBA light, dark;
theme_preferences_staged_get_color (THEME_TOKEN_TEXT_BACKGROUND, &light);
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
theme_preferences_staged_get_color (THEME_TOKEN_TEXT_BACKGROUND, &dark);
g_assert_false (gdk_rgba_equal (&light, &dark));
theme_preferences_stage_commit ();
g_assert_false (custom_tokens[THEME_TOKEN_TEXT_BACKGROUND]);
g_assert_true (custom_tokens[THEME_TOKEN_TEXT_FOREGROUND]);
gtk_widget_destroy (editor);
}
static void
test_contrast_suggestion_and_preview_navigation (void)
{
if (!gtk_available) { g_test_message ("GTK display not available"); return; }
memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens);
theme_preferences_stage_begin ();
GtkWidget *editor = theme_preferences_create_color_editor (NULL, NULL);
theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager");
theme_color_manager_row *row = g_ptr_array_index (ui->rows, THEME_TOKEN_TEXT_FOREGROUND);
GdkRGBA white, black, gray;
gdk_rgba_parse (&white, "#ffffff"); gdk_rgba_parse (&black, "#000000"); gdk_rgba_parse (&gray, "#dddddd");
g_assert_cmpfloat_with_epsilon (theme_preferences_contrast_ratio (&white, &black), 21.0, 0.001);
theme_preferences_manager_row_commit (row, &gray);
g_assert_true (gtk_widget_get_visible (ui->contrast_fix));
gtk_button_clicked (GTK_BUTTON (ui->contrast_fix));
theme_preferences_staged_get_color (THEME_TOKEN_TEXT_FOREGROUND, &gray);
g_assert_cmpfloat (theme_preferences_contrast_ratio (&gray, &white), >=, 4.5);
theme_preferences_manager_focus_color (ui, THEME_TOKEN_MIRC_12);
g_assert_true (gtk_expander_get_expanded (GTK_EXPANDER (ui->advanced)));
gtk_entry_set_text (GTK_ENTRY (row->entry), "invalid");
theme_preferences_manager_entry_commit (row);
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (row->entry)), ==, "invalid");
GError *error = NULL;
g_assert_false (theme_preferences_color_editor_stage (editor, &error));
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_clear_error (&error);
theme_preferences_stage_discard (); gtk_widget_destroy (editor);
}
static void
test_picker_cancel_restores_inheritance (void)
{
if (!gtk_available) { g_test_message ("GTK display not available"); return; }
memset (&prefs, 0, sizeof prefs); memset (custom_tokens, 0, sizeof custom_tokens);
theme_preferences_stage_begin ();
GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
GtkWidget *editor = theme_preferences_create_color_editor (GTK_WINDOW (window), NULL);
gtk_container_add (GTK_CONTAINER (window), editor);
theme_color_manager_ui *ui = g_object_get_data (G_OBJECT (editor), "zoitechat-theme-color-manager");
theme_color_manager_row *row = g_ptr_array_index (ui->rows, THEME_TOKEN_MIRC_12);
gtk_button_clicked (GTK_BUTTON (row->button));
GList *windows = gtk_window_list_toplevels ();
GtkWidget *picker = NULL;
for (GList *i = windows; i; i = i->next) if (GTK_IS_COLOR_CHOOSER_DIALOG (i->data)) picker = i->data;
g_assert_nonnull (picker);
GdkRGBA color; gdk_rgba_parse (&color, "#aabbcc");
gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (picker), &color);
g_assert_true (theme_preferences_stage.staged_custom[row->token]);
gtk_dialog_response (GTK_DIALOG (picker), GTK_RESPONSE_CANCEL);
g_assert_false (theme_preferences_stage.staged_custom[row->token]);
g_assert_false (custom_tokens[row->token]);
g_list_free (windows);
theme_preferences_stage_discard (); gtk_widget_destroy (window);
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
@@ -471,5 +676,10 @@ main (int argc, char **argv)
test_select_none_resets_theme_and_applies); test_select_none_resets_theme_and_applies);
g_test_add_func ("/theme/preferences/gtk3_select_theme_does_not_pin_palette_tokens", g_test_add_func ("/theme/preferences/gtk3_select_theme_does_not_pin_palette_tokens",
test_select_theme_does_not_pin_palette_tokens); test_select_theme_does_not_pin_palette_tokens);
g_test_add_func ("/theme/preferences/embedded_color_editor", test_embedded_color_editor);
g_test_add_func ("/theme/preferences/appearance_schemes_cancel", test_appearance_schemes_and_cancel);
g_test_add_func ("/theme/preferences/individual_reset_inherits", test_individual_reset_preserves_other_overrides);
g_test_add_func ("/theme/preferences/contrast_and_navigation", test_contrast_suggestion_and_preview_navigation);
g_test_add_func ("/theme/preferences/picker_cancel_inherits", test_picker_cancel_restores_inheritance);
return g_test_run (); return g_test_run ();
} }
@@ -430,6 +430,36 @@ test_save_writes_only_custom_token_keys (void)
g_free (cfg); g_free (cfg);
} }
static void
test_reset_one_colour_restores_inheritance_after_reload (void)
{
GdkRGBA color;
char *cfg;
setup_temp_home ();
theme_runtime_load ();
gdk_rgba_parse (&color, "#456789");
theme_runtime_user_set_color (THEME_TOKEN_TEXT_FOREGROUND, &color);
theme_runtime_user_set_color (THEME_TOKEN_TEXT_BACKGROUND, &color);
theme_runtime_reset_user_color (THEME_TOKEN_TEXT_BACKGROUND);
theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL);
g_assert_true (theme_runtime_save ());
cfg = read_colors_conf ();
g_assert_nonnull (strstr (cfg, "theme.mode.light.token.text_foreground"));
g_assert_null (strstr (cfg, "theme.mode.light.token.text_background"));
g_free (cfg);
theme_runtime_load ();
g_assert_false (theme_runtime_get_user_color (THEME_TOKEN_TEXT_BACKGROUND, &color));
g_assert_true (theme_runtime_get_user_color (THEME_TOKEN_TEXT_FOREGROUND, &color));
g_assert_cmpfloat_with_epsilon (color.red, 0x45 / 255.0, 0.0001);
ThemeGtkPaletteMap map = { .enabled = TRUE };
gdk_rgba_parse (&map.text_background, "#111111");
theme_runtime_get_default_color (THEME_TOKEN_TEXT_BACKGROUND, &map, &color);
g_assert_cmpfloat_with_epsilon (color.red, 0x11 / 255.0, 0.0001);
gdk_rgba_parse (&map.text_background, "#eeeeee");
theme_runtime_get_default_color (THEME_TOKEN_TEXT_BACKGROUND, &map, &color);
g_assert_cmpfloat_with_epsilon (color.red, 0xee / 255.0, 0.0001);
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
@@ -450,5 +480,6 @@ main (int argc, char **argv)
test_save_discard_unlinks_temp_file); test_save_discard_unlinks_temp_file);
g_test_add_func ("/theme/runtime/save_writes_only_custom_token_keys", g_test_add_func ("/theme/runtime/save_writes_only_custom_token_keys",
test_save_writes_only_custom_token_keys); test_save_writes_only_custom_token_keys);
g_test_add_func ("/theme/runtime/reset_colour_persists_inheritance", test_reset_one_colour_restores_inheritance_after_reload);
return g_test_run (); return g_test_run ();
} }
+8
View File
@@ -206,6 +206,14 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map
return TRUE; return TRUE;
} }
gboolean
theme_get_default_color_for_widget (GtkWidget *widget, ThemeSemanticToken token, GdkRGBA *color)
{
ThemeGtkPaletteMap map = { 0 };
theme_access_get_gtk_palette_map (widget, &map);
return theme_runtime_get_default_color (token, &map, color);
}
gboolean gboolean
theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
{ {
+1
View File
@@ -28,6 +28,7 @@
#include "theme-palette.h" #include "theme-palette.h"
#include "../xtext-color.h" #include "../xtext-color.h"
gboolean theme_get_default_color_for_widget (GtkWidget *widget, ThemeSemanticToken token, GdkRGBA *color);
gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba); gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba);
gboolean theme_get_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba); gboolean theme_get_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba);
gboolean theme_get_color_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue); gboolean theme_get_color_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue);
+18
View File
@@ -1223,9 +1223,27 @@ theme_gtk3_init (void)
gboolean gboolean
theme_gtk3_apply_current (GError **error) theme_gtk3_apply_current (GError **error)
{ {
if (!strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_LIGHT) || !strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK))
{
/* GTK ships Adwaita on every supported platform. Explicit Light/Dark
* choices must not depend on the desktop theme providing both variants. */
GtkSettings *settings = gtk_settings_get_default ();
theme_gtk3_disable ();
if (settings)
settings_apply_property (settings, "gtk-theme-name", "Adwaita");
settings_apply_for_variant (!strcmp (prefs.hex_gui_gtk3_theme, THEME_GTK3_BUILTIN_DARK) ?
THEME_GTK3_VARIANT_PREFER_DARK : THEME_GTK3_VARIANT_PREFER_LIGHT);
return TRUE;
}
if (!prefs.hex_gui_gtk3_theme[0]) if (!prefs.hex_gui_gtk3_theme[0])
{ {
theme_gtk3_disable (); theme_gtk3_disable ();
/* Restoring a previous theme's settings must not undo an explicit
* appearance choice, including when a settings notification re-enters. */
settings_apply_for_variant (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO ?
THEME_GTK3_VARIANT_FOLLOW_SYSTEM : prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_DARK ?
THEME_GTK3_VARIANT_PREFER_DARK : THEME_GTK3_VARIANT_PREFER_LIGHT);
return TRUE; return TRUE;
} }
+3
View File
@@ -24,6 +24,9 @@
#include <glib.h> #include <glib.h>
#include <gdk/gdk.h> #include <gdk/gdk.h>
#define THEME_GTK3_BUILTIN_LIGHT "builtin:light"
#define THEME_GTK3_BUILTIN_DARK "builtin:dark"
typedef enum typedef enum
{ {
THEME_GTK3_VARIANT_FOLLOW_SYSTEM = 0, THEME_GTK3_VARIANT_FOLLOW_SYSTEM = 0,
File diff suppressed because it is too large. Load diff
+3
View File
@@ -37,5 +37,8 @@ void theme_preferences_stage_begin (void);
void theme_preferences_stage_apply (void); void theme_preferences_stage_apply (void);
void theme_preferences_stage_commit (void); void theme_preferences_stage_commit (void);
void theme_preferences_stage_discard (void); void theme_preferences_stage_discard (void);
void theme_preferences_stage_revert_runtime (void);
GtkWidget *theme_preferences_create_color_editor (GtkWindow *parent, gboolean *color_change_flag);
gboolean theme_preferences_color_editor_stage (GtkWidget *editor, GError **error);
#endif #endif
+31
View File
@@ -367,6 +367,37 @@ theme_runtime_get_xtext_colors_mapped (const ThemeGtkPaletteMap *gtk_map, XTextC
theme_palette_to_xtext_colors (&mapped_palette, palette, palette_len); theme_palette_to_xtext_colors (&mapped_palette, palette, palette_len);
} }
gboolean
theme_runtime_get_user_color (ThemeSemanticToken token, GdkRGBA *color)
{
if (token < 0 || token >= THEME_TOKEN_COUNT || !light_custom_tokens[token])
return FALSE;
return theme_palette_get_color (&light_palette, token, color);
}
gboolean
theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color)
{
ThemePalette defaults;
gboolean custom[THEME_TOKEN_COUNT] = { FALSE };
theme_palette_from_legacy_colors (&defaults, legacy_light_defaults, G_N_ELEMENTS (legacy_light_defaults));
theme_runtime_apply_gtk_map (&defaults, map, custom);
return theme_palette_get_color (&defaults, token, color);
}
void
theme_runtime_reset_user_color (ThemeSemanticToken token)
{
GdkRGBA color;
if (!theme_runtime_get_default_color (token, NULL, &color))
return;
if (!user_colors_valid)
light_palette = active_palette;
theme_palette_set_color (&light_palette, token, &color);
light_custom_tokens[token] = FALSE;
user_colors_valid = TRUE;
}
void void
theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col) theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col)
{ {
+3
View File
@@ -44,6 +44,9 @@ gboolean theme_runtime_save_finalize (const char *temp_path);
void theme_runtime_save_discard (const char *temp_path); void theme_runtime_save_discard (const char *temp_path);
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed); gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed);
gboolean theme_runtime_apply_dark_mode (gboolean enable); gboolean theme_runtime_apply_dark_mode (gboolean enable);
gboolean theme_runtime_get_user_color (ThemeSemanticToken token, GdkRGBA *color);
void theme_runtime_reset_user_color (ThemeSemanticToken token);
gboolean theme_runtime_get_default_color (ThemeSemanticToken token, const ThemeGtkPaletteMap *map, GdkRGBA *color);
void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col); void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col);
void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col); void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col);
void theme_runtime_reset_mode_colors (gboolean dark_mode); void theme_runtime_reset_mode_colors (gboolean dark_mode);
+6 -2
View File
@@ -2713,7 +2713,7 @@ gtk_xtext_selection_get_text (GtkXText *xtext, int *len_ret)
first = FALSE; first = FALSE;
if (ent->mark_end > ent->mark_start) if (ent->mark_end > ent->mark_start)
{ {
if (ent->mark_start == 0 && xtext->mark_stamp) if (ent->mark_start == 0 && xtext->mark_stamp && ent->stamp_len == 0)
{ {
char *stamp; char *stamp;
int len = xtext_get_stamp_str (ent->stamp, &stamp); int len = xtext_get_stamp_str (ent->stamp, &stamp);
@@ -4038,7 +4038,11 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize); xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
} }
if (entline == 1 && xtext->auto_indent && xtext->buffer->time_stamp && /* Preserve margin stamps across indent changes, including plain
* entries with no inline timestamp. Never draw a second stamp
* for entries that already contain an inline timestamp. */
if (entline == 1 && (xtext->auto_indent || ent->left_len != -1) &&
ent->stamp_len == 0 && xtext->buffer->time_stamp &&
(!xtext->skip_stamp || xtext->mark_stamp || xtext->force_stamp)) (!xtext->skip_stamp || xtext->mark_stamp || xtext->force_stamp))
{ {
char *time_str; char *time_str;