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
67 changed files with 7051 additions and 2781 deletions

No files matched your search

+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
build-essential pkg-config meson ninja-build cmake \
gettext \
libcanberra-dev libglib2.0-dev \
libarchive-dev libxss-dev \
libarchive-dev \
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
libxkbcommon0 \
@@ -57,7 +57,7 @@ jobs:
ninja -C build
- name: Test emoji catalog, font coverage and picker
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Autoaway Tests" "Idle Backend Tests" "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
- name: Install to AppDir
run: |
+1 -5
View File
@@ -198,7 +198,7 @@ jobs:
)
shell: cmd
- name: Test autoaway, emoji catalog, font coverage and picker
- name: Test emoji catalog, font coverage and picker
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -213,10 +213,6 @@ jobs:
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
if errorlevel 1 exit /b 1
)
msbuild win32\autoaway-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64
if errorlevel 1 exit /b 1
..\zoitechat-build\x64\tests\autoaway-tests.exe --tap
if errorlevel 1 exit /b 1
- name: Preparing Artifacts
run: |
+1 -5
View File
@@ -209,7 +209,7 @@ jobs:
# Master added native emoji font staging and tests after the release
# branch split. Run these before packaging so MSI/MSIX use verified files.
- name: Test autoaway, emoji catalog, font coverage and picker
- name: Test emoji catalog, font coverage and picker
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -224,10 +224,6 @@ jobs:
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
if errorlevel 1 exit /b 1
)
msbuild win32\autoaway-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64
if errorlevel 1 exit /b 1
..\zoitechat-build\x64\tests\autoaway-tests.exe --tap
if errorlevel 1 exit /b 1
- name: Install WiX Toolset
shell: pwsh
+2 -4
View File
@@ -138,10 +138,8 @@ html_context = {
# of the sidebar.
html_logo = '_static/zoitechat-logo.svg'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = '_static/favicon.ico'
# Use the ZoiteChat logo instead of the old HexChat favicon.
html_favicon = '_static/zoitechat-logo.svg'
# 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,
-35
View File
@@ -628,38 +628,3 @@ List of Settings
+------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| **url\_logging** | Toggle logging URLs to *<config>/url.log*. |
+------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Autoaway
--------
In **Preferences > Chatting > General > Away**, set **Autoaway after** to a
number of minutes. The default is 0 (disabled). The separate **Autoaway
message** defaults to "Idle". No plugin or scripting language is required.
The same options are available immediately through ``/SET``::
/SET away_auto_timeout 10
/SET away_auto_reason Idle
/SET away_auto_system ON
With desktop activity enabled, Windows uses the current login session's last
input time, macOS uses CoreGraphics input time, and native X11 uses the
XScreenSaver extension if its development library was available at build time.
Debian/Ubuntu builders can install ``libxss-dev``; Fedora builders can install
``libXScrnSaver-devel``. Meson detects this optional dependency automatically.
On native Wayland, XWayland, unsupported desktops, and builds without XScreenSaver,
autoaway falls back to keyboard, mouse, scroll and touch activity **inside
ZoiteChat**. Activity in other applications does not reset that fallback timer.
Set ``away_auto_system OFF`` to always use ZoiteChat activity on every platform.
The text frontend uses submitted user input. Network traffic, redraws and window
focus changes do not count as user activity.
Autoaway checks every five seconds, after IRC login finishes, independently on
each connected network. Returning clears only away states set by autoaway.
Manual ``/AWAY`` (including ``/AWAY`` while already automatically away) takes
ownership, so activity does not clear it. ``/BACK`` resets the inactivity timer.
The existing **Automatically unmark away** setting keeps its separate behaviour
when sending messages. Do not run another autoaway script at the same time.
Disabling autoaway clears its own away states. Reconnecting re-evaluates current
inactivity instead of restoring an outdated automatic away state; manual away
restoration is unchanged.
+1 -1
View File
@@ -55,7 +55,7 @@
"run-tests": true,
"test-rule": "",
"test-commands": [
"meson test --print-errorlogs 'Autoaway Tests' 'Idle Backend Tests' 'Emoji Data Tests' 'Emoji Font Tests'"
"meson test --print-errorlogs 'Emoji Data Tests' 'Emoji Font Tests'"
],
"config-opts": [
"-Ddbus-service-use-appid=true",
-1
View File
@@ -25,7 +25,6 @@ BuildRequires: pkgconfig(gmodule-2.0)
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(fontconfig) >= 2.13
BuildRequires: pkgconfig(pangoft2) >= 1.44
BuildRequires: pkgconfig(xscrnsaver)
BuildRequires: pkgconfig(iso-codes)
BuildRequires: pkgconfig(libarchive)
BuildRequires: pkgconfig(libcanberra) >= 0.22
-135
View File
@@ -1,135 +0,0 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "zoitechat.h"
#include "zoitechatc.h"
#include "server.h"
#include "fe.h"
#include "autoaway.h"
static gint64 last_activity;
static int timer_tag;
static int previous_timeout;
static unsigned int previous_system;
void
autoaway_activity (void)
{
last_activity = g_get_monotonic_time ();
}
/* A manual /AWAY takes ownership even if the server already says away. */
void
autoaway_manual (server *serv)
{
serv->autoaway_owned = FALSE;
autoaway_activity ();
}
void
autoaway_reset (server *serv)
{
if (!serv->autoaway_owned)
return;
serv->autoaway_owned = FALSE;
serv->reconnect_away = FALSE;
g_clear_pointer (&serv->last_away_reason, g_free);
}
static void
autoaway_release (server *serv)
{
if (!serv->autoaway_owned)
return;
/* Cancel even an unacknowledged AWAY. IRC replies may arrive later. */
if (serv->connected && serv->end_of_motd)
serv->p_set_back (serv);
autoaway_reset (serv);
}
void
autoaway_check (gint64 now, gint64 desktop_idle)
{
GSList *list;
gint64 idle = MAX ((gint64)0, now - last_activity);
gint64 limit = (gint64)prefs.hex_away_auto_timeout * 60 * G_USEC_PER_SEC;
if (prefs.hex_away_auto_system && desktop_idle >= 0)
idle = MIN (idle, desktop_idle);
for (list = serv_list; list; list = list->next)
{
server *serv = list->data;
if (!serv->connected)
{
/* Re-evaluate idle after login instead of restoring stale autoaway. */
autoaway_reset (serv);
continue;
}
if (!serv->end_of_motd)
continue;
if (limit <= 0 || idle < limit)
{
autoaway_release (serv);
continue;
}
if (serv->is_away || serv->reconnect_away)
continue;
/* Retry a rejected/lost request slowly, never once per poll. */
if (serv->autoaway_owned &&
now - serv->autoaway_sent_at < 30 * G_USEC_PER_SEC)
continue;
g_free (serv->last_away_reason);
serv->last_away_reason = g_strdup (prefs.hex_away_auto_reason[0] ?
prefs.hex_away_auto_reason : _("Idle"));
serv->autoaway_owned = TRUE;
serv->autoaway_sent_at = now;
serv->p_set_away (serv, serv->last_away_reason);
}
}
static int
autoaway_timer (void)
{
autoaway_check (g_get_monotonic_time (),
prefs.hex_away_auto_system ? fe_get_idle_time () : -1);
return 1;
}
void
autoaway_reinit (void)
{
if (previous_timeout != prefs.hex_away_auto_timeout ||
previous_system != prefs.hex_away_auto_system)
{
autoaway_activity ();
previous_timeout = prefs.hex_away_auto_timeout;
previous_system = prefs.hex_away_auto_system;
}
if (prefs.hex_away_auto_timeout > 0)
{
if (!timer_tag)
{
autoaway_activity ();
timer_tag = fe_timeout_add_seconds (5, autoaway_timer, NULL);
}
}
else
{
if (timer_tag)
{
fe_timeout_remove (timer_tag);
timer_tag = 0;
}
/* Disabling autoaway must also work without another input event. */
autoaway_check (g_get_monotonic_time (), -1);
}
}
-19
View File
@@ -1,19 +0,0 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef ZOITECHAT_AUTOAWAY_H
#define ZOITECHAT_AUTOAWAY_H
#include <glib.h>
struct server;
void autoaway_activity (void);
void autoaway_manual (struct server *serv);
void autoaway_reset (struct server *serv);
void autoaway_reinit (void);
/* Both samples are in microseconds; -1 means desktop idle is unavailable. */
void autoaway_check (gint64 now, gint64 desktop_idle);
#endif
-6
View File
@@ -29,7 +29,6 @@
#include "fe.h"
#include "text.h"
#include "zoitechatc.h"
#include "autoaway.h"
#include "typedef.h"
#ifdef WIN32
@@ -360,9 +359,6 @@ default_file (void)
const struct prefs vars[] =
{
{"away_auto_reason", P_OFFSET (hex_away_auto_reason), TYPE_STR},
{"away_auto_system", P_OFFINT (hex_away_auto_system), TYPE_BOOL, autoaway_reinit},
{"away_auto_timeout", P_OFFINT (hex_away_auto_timeout), TYPE_INT, autoaway_reinit},
{"away_auto_unmark", P_OFFINT (hex_away_auto_unmark), TYPE_BOOL},
{"away_omit_alerts", P_OFFINT (hex_away_omit_alerts), TYPE_BOOL},
{"away_reason", P_OFFSET (hex_away_reason), TYPE_STR},
@@ -771,7 +767,6 @@ load_default_config(void)
/* put in default values, anything left out is automatically zero */
/* BOOLEANS */
prefs.hex_away_auto_system = 1;
prefs.hex_away_show_once = 1;
prefs.hex_away_track = 1;
prefs.hex_dcc_auto_resume = 1;
@@ -892,7 +887,6 @@ load_default_config(void)
prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */
/* STRINGS */
strcpy (prefs.hex_away_auto_reason, _("Idle"));
strcpy (prefs.hex_away_reason, _("I'm busy"));
strcpy (prefs.hex_completion_suffix, ",");
#ifdef WIN32
-2
View File
@@ -11,7 +11,6 @@
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClInclude Include="autoaway.h" />
<ClInclude Include="cfgfiles.h" />
<ClInclude Include="chanopt.h" />
<ClInclude Include="ctcp.h" />
@@ -53,7 +52,6 @@
<ClInclude Include="gtk3-theme-service.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="autoaway.c" />
<ClCompile Include="cfgfiles.c" />
<ClCompile Include="chanopt.c" />
<ClCompile Include="ctcp.c" />
-6
View File
@@ -17,9 +17,6 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="autoaway.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="cfgfiles.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -139,9 +136,6 @@
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="autoaway.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="cfgfiles.c">
<Filter>Source Files</Filter>
</ClCompile>
-1
View File
@@ -142,7 +142,6 @@ void fe_lastlog (session *sess, session *lastlog_sess, char *sstr, gtk_xtext_sea
void fe_set_lag (server *serv, long lag);
void fe_set_throttle (server *serv);
void fe_set_away (server *serv);
gint64 fe_get_idle_time (void);
void fe_serverlist_open (session *sess);
void fe_get_bool (char *title, char *prompt, void *callback, void *userdata);
void fe_get_str (char *prompt, char *def, void *callback, void *ud);
-8
View File
@@ -1,5 +1,4 @@
common_sources = [
'autoaway.c',
'cfgfiles.c',
'chanopt.c',
'ctcp.c',
@@ -225,10 +224,3 @@ test('Hex Color Tests', hexcolor_tests,
protocol: 'tap',
timeout: 120,
)
autoaway_tests = executable('autoaway_tests',
['tests/test-autoaway.c', 'autoaway.c'],
include_directories: [config_h_include, include_directories('.')],
dependencies: zoitechat_common_header_deps,
)
test('Autoaway Tests', autoaway_tests, protocol: 'tap', timeout: 30)
+1 -12
View File
@@ -54,7 +54,6 @@
#include "server.h"
#include "tree.h"
#include "outbound.h"
#include "autoaway.h"
#include "chanopt.h"
#define TBUFSIZE 4096
@@ -361,10 +360,6 @@ cmd_away (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{
char *reason = word_eol[2];
/* Restoring a manual away after reconnect is not new user input. */
if (!sess->server->reconnect_away)
autoaway_manual (sess->server);
if (!(*reason))
{
if (sess->server->is_away)
@@ -401,10 +396,7 @@ cmd_away (struct session *sess, char *tbuf, char *word[], char *word_eol[])
static int
cmd_back (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{
gboolean was_autoaway = sess->server->autoaway_owned;
autoaway_manual (sess->server);
if (sess->server->is_away || was_autoaway)
if (sess->server->is_away)
{
sess->server->p_set_back (sess->server);
}
@@ -5059,9 +5051,6 @@ handle_user_input (session *sess, char *text, int history, int nocommand)
if (*text == '\0')
return 1;
if (history)
autoaway_activity ();
if (history)
history_add (&sess->history, text);
-7
View File
@@ -30,7 +30,6 @@
#include "zoitechat.h"
#include "proto-irc.h"
#include "autoaway.h"
#include "ctcp.h"
#include "fe.h"
#include "ignore.h"
@@ -439,12 +438,6 @@ irc_raw (server *serv, char *raw)
{
int len;
char tbuf[8704];
/* /QUOTE AWAY and scripts sending raw AWAY also take manual ownership. */
if (!g_ascii_strncasecmp (raw, "AWAY", 4) &&
(raw[4] == 0 || raw[4] == ' '))
autoaway_manual (serv);
if (*raw)
{
len = strlen (raw);
-2
View File
@@ -54,7 +54,6 @@
#include "proto-irc.h"
#include "servlist.h"
#include "server.h"
#include "autoaway.h"
#include "sts.h"
#ifdef USE_OPENSSL
@@ -1874,7 +1873,6 @@ is_server (server *serv)
void
server_set_defaults (server *serv)
{
autoaway_reset (serv);
g_free (serv->chantypes);
g_clear_pointer (&serv->clienttagdeny, g_free);
g_free (serv->chanmodes);
-296
View File
@@ -1,296 +0,0 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <string.h>
#include "../zoitechat.h"
#include "../autoaway.h"
struct zoitechatprefs prefs;
GSList *serv_list;
static server servers[3];
static int away_count, back_count, timer_adds, timer_removes;
static gint64 base;
static char *sent_reason;
/* Protocol stubs deliberately do not acknowledge requests. */
static void
send_away (server *serv, char *reason)
{
away_count++;
g_free (sent_reason);
sent_reason = g_strdup (reason);
}
static void
send_back (server *serv)
{
back_count++;
}
int
fe_timeout_add_seconds (int interval, void *callback, void *data)
{
g_assert_cmpint (interval, ==, 5);
timer_adds++;
return 42;
}
void
fe_timeout_remove (int tag)
{
g_assert_cmpint (tag, ==, 42);
timer_removes++;
}
gint64
fe_get_idle_time (void)
{
return -1;
}
static void
setup (void)
{
int i;
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
for (i = 0; i < 3; i++)
g_free (servers[i].last_away_reason);
g_slist_free (serv_list);
serv_list = NULL;
memset (servers, 0, sizeof servers);
memset (&prefs, 0, sizeof prefs);
away_count = back_count = timer_adds = timer_removes = 0;
g_clear_pointer (&sent_reason, g_free);
prefs.hex_away_auto_timeout = 10;
prefs.hex_away_auto_system = TRUE;
g_strlcpy (prefs.hex_away_auto_reason, "Auto idle", sizeof prefs.hex_away_auto_reason);
for (i = 0; i < 3; i++)
{
servers[i].connected = TRUE;
servers[i].end_of_motd = TRUE;
servers[i].p_set_away = send_away;
servers[i].p_set_back = send_back;
}
serv_list = g_slist_append (NULL, &servers[0]);
autoaway_activity ();
base = g_get_monotonic_time ();
}
static void
test_fallback_boundary (void)
{
setup ();
autoaway_check (base + 599 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 0);
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
g_assert_true (servers[0].autoaway_owned);
g_assert_cmpstr (sent_reason, ==, "Auto idle");
/* An ACK is not required to avoid flooding the connection. */
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[0].is_away = TRUE;
autoaway_check (base + 640 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
g_assert_false (servers[0].autoaway_owned);
g_assert_null (servers[0].last_away_reason);
}
static void
test_desktop_activity (void)
{
setup ();
autoaway_check (base + 900 * G_USEC_PER_SEC, 5 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 0);
autoaway_check (base + 900 * G_USEC_PER_SEC, 700 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 1);
servers[0].is_away = TRUE;
autoaway_check (base + 905 * G_USEC_PER_SEC, 0);
g_assert_cmpint (back_count, ==, 1);
/* App input wins even if the native backend still has an old sample. */
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), 900 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 1);
}
static void
test_app_mode (void)
{
setup ();
prefs.hex_away_auto_system = FALSE;
autoaway_check (base + 600 * G_USEC_PER_SEC, 0);
g_assert_cmpint (away_count, ==, 1);
}
static void
test_manual_away (void)
{
setup ();
servers[0].is_away = TRUE;
servers[0].last_away_reason = g_strdup ("Lunch");
autoaway_check (base + 900 * G_USEC_PER_SEC, -1);
autoaway_check (base + 905 * G_USEC_PER_SEC, 0);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (away_count, ==, 0);
g_assert_cmpint (back_count, ==, 0);
g_assert_cmpstr (servers[0].last_away_reason, ==, "Lunch");
}
static void
test_manual_takeover (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
servers[0].is_away = TRUE;
/* This is also called for /AWAY with no reason, even if already away. */
autoaway_manual (&servers[0]);
autoaway_check (g_get_monotonic_time (), 0);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 0);
g_assert_true (servers[0].is_away);
}
static void
test_pending_away_cancel (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_false (servers[0].is_away);
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
/* A late 306 must not make us own the away state again. */
servers[0].is_away = TRUE;
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
}
static void
test_login_and_multiple_servers (void)
{
setup ();
serv_list = g_slist_append (serv_list, &servers[1]);
serv_list = g_slist_append (serv_list, &servers[2]);
servers[1].is_away = TRUE;
servers[1].last_away_reason = g_strdup ("Manual");
servers[2].end_of_motd = FALSE;
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[2].end_of_motd = TRUE;
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
autoaway_check (base + 610 * G_USEC_PER_SEC, 0);
g_assert_cmpint (back_count, ==, 2);
g_assert_cmpstr (servers[1].last_away_reason, ==, "Manual");
}
static void
test_disconnect_and_reconnect (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
servers[0].connected = FALSE;
servers[0].is_away = TRUE;
servers[0].reconnect_away = TRUE;
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_false (servers[0].autoaway_owned);
g_assert_false (servers[0].reconnect_away);
g_assert_null (servers[0].last_away_reason);
g_assert_cmpint (back_count, ==, 0);
servers[0].is_away = FALSE;
servers[0].connected = TRUE;
servers[0].end_of_motd = FALSE;
autoaway_check (base + 610 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[0].end_of_motd = TRUE;
autoaway_check (base + 615 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
/* server_set_defaults resets ownership even before the next timer tick. */
servers[0].reconnect_away = TRUE;
autoaway_reset (&servers[0]);
g_assert_false (servers[0].reconnect_away);
/* Existing manual reconnect-away must be left alone. */
servers[0].reconnect_away = TRUE;
servers[0].last_away_reason = g_strdup ("Manual");
autoaway_reset (&servers[0]);
g_assert_true (servers[0].reconnect_away);
g_assert_cmpstr (servers[0].last_away_reason, ==, "Manual");
}
static void
test_retry (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
autoaway_check (base + 629 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
autoaway_check (base + 630 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
}
static void
test_disable_and_timers (void)
{
setup ();
autoaway_reinit ();
autoaway_reinit ();
g_assert_cmpint (timer_adds, ==, 1);
base = g_get_monotonic_time ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 1);
g_assert_cmpint (timer_removes, ==, 1);
g_assert_false (servers[0].autoaway_owned);
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 1);
prefs.hex_away_auto_timeout = -1;
autoaway_reinit ();
g_assert_cmpint (away_count, ==, 1);
}
static void
test_empty_reason_and_large_timeout (void)
{
setup ();
prefs.hex_away_auto_reason[0] = 0;
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_nonnull (sent_reason);
g_assert_cmpstr (sent_reason, !=, "");
setup ();
prefs.hex_away_auto_timeout = G_MAXINT;
autoaway_check (base + 900 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 0);
}
int
main (int argc, char **argv)
{
int result, i;
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/autoaway/fallback-boundary", test_fallback_boundary);
g_test_add_func ("/autoaway/desktop-activity", test_desktop_activity);
g_test_add_func ("/autoaway/app-mode", test_app_mode);
g_test_add_func ("/autoaway/manual-away", test_manual_away);
g_test_add_func ("/autoaway/manual-takeover", test_manual_takeover);
g_test_add_func ("/autoaway/pending-cancel", test_pending_away_cancel);
g_test_add_func ("/autoaway/login-multiple-servers", test_login_and_multiple_servers);
g_test_add_func ("/autoaway/reconnect", test_disconnect_and_reconnect);
g_test_add_func ("/autoaway/retry", test_retry);
g_test_add_func ("/autoaway/disable-timers", test_disable_and_timers);
g_test_add_func ("/autoaway/empty-reason-large-timeout", test_empty_reason_and_large_timeout);
result = g_test_run ();
for (i = 0; i < 3; i++)
g_free (servers[i].last_away_reason);
g_free (sent_reason);
g_slist_free (serv_list);
return result;
}
+48 -22
View File
@@ -68,7 +68,7 @@ static ca_context *ca_con;
#define SCROLLBACK_MAX 32000
static void mkdir_p (char *filename);
static char *log_create_filename (char *channame);
static char *log_create_filename (const char *channame);
static char *
scrollback_get_filename (session *sess)
@@ -396,7 +396,7 @@ mkdir_p (char *filename)
}
static char *
log_create_filename (char *channame)
log_create_filename (const char *channame)
{
char *tmp, *ret;
int mbl;
@@ -453,7 +453,7 @@ log_escape_strcpy (char *dest, char *src, char *end)
/* substitutes %c %n %s into buffer */
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;
@@ -466,6 +466,14 @@ log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
return;
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++;
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);
break;
default:
if (buf + 1 == end)
{
buf[0] = 0;
return;
}
buf[0] = '%';
buf++;
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 *
log_create_pathname (char *servname, char *channame, char *netname)
char *
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 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)
{
netname = g_strdup ("NETWORK");
safe_network = g_strdup ("NETWORK");
}
else
{
netname = log_create_filename (netname);
safe_network = log_create_filename (netname);
}
/* first, everything is in UTF-8 */
if (!rfc_casecmp (channame, servname))
{
channame = g_strdup ("server");
safe_channel = g_strdup ("server");
}
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);
g_free (channame);
g_free (netname);
g_free (servname);
log_insert_vars (fname, sizeof (fname), mask, safe_channel, safe_network, safe_server);
g_free (safe_channel);
g_free (safe_network);
g_free (safe_server);
/* insert time/date */
now = time (NULL);
strftime_utf8 (fnametime, sizeof (fnametime), fname, now);
if (!g_utf8_validate (fname, -1, NULL) ||
!strftime_utf8 (fnametime, sizeof (fnametime), fname, now))
return NULL;
/* 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
* 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);
}
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);
}
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
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 log_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 pevent_save (char *fn);
int pevt_build_string (const char *input, char **output, int *max_arg);
-3
View File
@@ -46,7 +46,6 @@
#include "plugin.h"
#include "plugin-identd.h"
#include "plugin-timer.h"
#include "autoaway.h"
#include "notify.h"
#include "server.h"
#include "servlist.h"
@@ -500,8 +499,6 @@ zoitechat_reinit_timers (void)
static int lag_check_tag = 0;
static int away_tag = 0;
autoaway_reinit ();
if (prefs.hex_notify_timeout && notify_tag == 0)
{
notify_tag = fe_timeout_add_seconds (prefs.hex_notify_timeout,
-5
View File
@@ -105,7 +105,6 @@ struct zoitechatprefs
/* these are the rebranded, consistent, sorted zoitechat variables */
/* BOOLEANS */
unsigned int hex_away_auto_system;
unsigned int hex_away_auto_unmark;
unsigned int hex_away_omit_alerts;
unsigned int hex_away_show_once;
@@ -234,7 +233,6 @@ struct zoitechatprefs
unsigned int hex_url_logging;
/* NUMBERS */
int hex_away_auto_timeout;
int hex_away_size_max;
int hex_away_timeout;
int hex_completion_amount;
@@ -313,7 +311,6 @@ struct zoitechatprefs
int hex_url_grabber_limit;
/* STRINGS */
char hex_away_auto_reason[256];
char hex_away_reason[256];
char hex_completion_suffix[4]; /* Only ever holds a one-character string. */
char hex_dcc_completed_dir[PATHLEN + 1];
@@ -600,7 +597,6 @@ typedef struct server
/*time_t connect_time;*/ /* when did it connect? */
unsigned long lag_sent; /* we are still waiting for this ping response*/
time_t ping_recv; /* when we last got a ping reply */
gint64 autoaway_sent_at;
time_t away_time; /* when we were marked away */
char *encoding;
@@ -620,7 +616,6 @@ typedef struct server
unsigned int end_of_motd:1; /* end of motd reached (logged in) */
unsigned int sent_quit:1; /* sent a QUIT already? */
unsigned int use_listargs:1; /* undernet and dalnet need /list >0,<10000 */
unsigned int autoaway_owned:1;
unsigned int is_away:1;
unsigned int reconnect_away:1; /* whether to reconnect in is_away state */
unsigned int dont_use_proxy:1; /* to proxy or not to proxy */
+300
View File
@@ -41,6 +41,8 @@
#include "gtkutil.h"
#include "maingui.h"
#include "editlist.h"
#include "preferences-persistence.h"
#include "setup.h"
#define ICON_EDITLIST_NEW "document-new"
#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);
}
/* 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
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
-2
View File
@@ -59,7 +59,6 @@
#include "notifygui.h"
#include "textgui.h"
#include "fkeys.h"
#include "idle.h"
#include "plugin-tray.h"
#include "urlgrab.h"
#include "setup.h"
@@ -639,7 +638,6 @@ fe_init (void)
{
theme_manager_init ();
key_init ();
idle_init ();
pixmaps_init ();
#ifdef HAVE_GTK_MAC
+3 -3
View File
@@ -31,7 +31,7 @@
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;user32.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
</Link>
</ItemDefinitionGroup>
@@ -49,7 +49,6 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="idle.h" />
<ClInclude Include="ascii.h" />
<ClInclude Include="banlist.h" />
<ClInclude Include="chanlist.h" />
@@ -64,6 +63,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="joind.h" />
<ClInclude Include="maingui.h" />
<ClInclude Include="preferences-persistence.h" />
<ClInclude Include="preferences-pages.h" />
<ClInclude Include="menu.h" />
<ClInclude Include="notifications\notification-backend.h" />
<ClInclude Include="notifygui.h" />
@@ -92,7 +92,6 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="theme\theme-preferences.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="idle.c" />
<ClCompile Include="ascii.c" />
<ClCompile Include="banlist.c" />
<ClCompile Include="chanlist.c" />
@@ -115,6 +114,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClCompile Include="notifygui.c" />
<ClCompile Include="pixmaps.c" />
<ClCompile Include="preferences-persistence.c" />
<ClCompile Include="preferences-pages.c" />
<ClCompile Include="plugin-notification.c" />
<ClCompile Include="plugin-tray.c" />
<ClCompile Include="plugingui.c" />
+6 -6
View File
@@ -15,9 +15,6 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="idle.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ascii.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -60,6 +57,9 @@
<ClInclude Include="preferences-persistence.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="preferences-pages.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="menu.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -140,9 +140,6 @@
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="idle.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ascii.c">
<Filter>Source Files</Filter>
</ClCompile>
@@ -200,6 +197,9 @@
<ClCompile Include="preferences-persistence.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="preferences-pages.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="plugingui.c">
<Filter>Source Files</Filter>
</ClCompile>
+143 -672
View File
@@ -58,6 +58,9 @@
#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_DELETE "edit-delete"
@@ -110,7 +113,7 @@ struct gcomp_data
static int key_load_kbs (void);
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_reset (GtkWidget *wid, gpointer userdata);
static int key_action_handle_command (GtkWidget * wid, GdkEventKey * evt,
@@ -159,53 +162,6 @@ static int key_action_reopen_closed_tab (GtkWidget * wid, GdkEventKey * evt,
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
key_action_decode_escapes (const char *input, char *output)
{
@@ -365,69 +321,6 @@ static const struct key_action key_actions[KEY_MAX_ACTIONS + 1] = {
"ACCEL=F1\nMenu Shortcut\nD1:contents\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
key_init (void)
{
@@ -619,13 +512,13 @@ key_get_menu_accel (const char *name, guint *keyval, GdkModifierType *mod)
enum
{
KEY_COLUMN,
ACCEL_COLUMN,
ACTION_COLUMN,
D1_COLUMN,
D2_COLUMN,
CUSTOM_COLUMN,
N_COLUMNS
KEY_COLUMN = PREFERENCES_SHORTCUT_LABEL,
ACCEL_COLUMN = PREFERENCES_SHORTCUT_ACCEL,
ACTION_COLUMN = PREFERENCES_SHORTCUT_ACTION,
D1_COLUMN = PREFERENCES_SHORTCUT_DATA1,
D2_COLUMN = PREFERENCES_SHORTCUT_DATA2,
CUSTOM_COLUMN = PREFERENCES_SHORTCUT_CUSTOM,
N_COLUMNS = PREFERENCES_SHORTCUT_N_COLUMNS
};
static GtkWidget *key_dialog = NULL;
@@ -633,280 +526,15 @@ static GtkWidget *key_dialog = NULL;
static inline GtkTreeModel *
get_store (void)
{
return gtk_tree_view_get_model (g_object_get_data (G_OBJECT (key_dialog), "view"));
}
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);
return g_object_get_data (G_OBJECT (key_dialog), "bindings-store");
}
static char *
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
@@ -1031,309 +659,152 @@ key_dialog_reset (GtkWidget *wid, gpointer userdata)
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
key_dialog_load (GtkListStore *store)
{
struct key_binding *kb = NULL;
char *label_text, *accel_text;
GtkTreeIter iter;
GSList *list = keybind_list;
while (list)
GHashTable *defaults = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
GHashTable *seen = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
GSList *reference = NULL;
key_load_kbs_from_buffer (g_strdup (default_kb_cfg), strlen (default_kb_cfg), &reference);
for (GSList *item = reference; item; item = item->next)
{
kb = (struct key_binding*)list->data;
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);
struct key_binding *kb = item->data;
key_dialog_reset_increment (defaults, key_binding_signature (key_actions[kb->action].name, kb->data1, kb->data2));
}
/* 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
key_dialog_show (void)
{
GtkWidget *vbox, *box, *pane, *scroll;
GtkWidget *view, *xtext, *delete_button;
GtkListStore *store;
XTextColor xtext_palette[XTEXT_COLS];
char buf[128];
if (key_dialog)
{
mg_bring_tofront (key_dialog);
return;
setup_open_page ("keyboard");
}
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 char *
key_preferences_serialize (GtkTreeModel *model, GError **error)
{
GtkTreeIter iter;
GString *text = g_string_new ("# ZoiteChat key bindings config file\n\n");
gboolean valid = gtk_tree_model_get_iter_first (model, &iter);
while (valid)
{
char *accel, *action, *data1, *data2;
guint key;
GdkModifierType modifiers;
gtk_tree_model_get (model, &iter, ACCEL_COLUMN, &accel, ACTION_COLUMN, &action,
D1_COLUMN, &data1, D2_COLUMN, &data2, -1);
if ((!accel || !*accel) && (!action || !*action) && (!data1 || !*data1) && (!data2 || !*data2))
goto next;
gtk_accelerator_parse (accel ? accel : "", &key, &modifiers);
if (!key || !action || strcmp (key_actions[key_get_action_from_string (action)].name, action) != 0 ||
(data1 && strpbrk (data1, "\r\n")) || (data2 && strpbrk (data2, "\r\n")))
{
GtkTreeView *view = g_object_get_data (G_OBJECT (key_dialog), "view");
GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
setup_reveal_widget (GTK_WIDGET (view));
preferences_shortcuts_reveal_path (key_dialog, path);
gtk_widget_grab_focus (GTK_WIDGET (view));
gtk_tree_path_free (path);
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
_("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);
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);
}
static int
key_save_kbs (void)
gboolean
key_preferences_stage (GError **error)
{
int fd;
char buf[512];
char *accel_text;
GSList *list = keybind_list;
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)
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)
{
kb = list->data;
accel_text = gtk_accelerator_name (kb->keyval, kb->mod);
g_snprintf (buf, 510, "ACCEL=%s\n%s\n", accel_text, key_actions[kb->action].name);
write (fd, buf, strlen (buf));
g_free (accel_text);
if (kb->data1 && kb->data1[0])
write (fd, buf, g_snprintf (buf, 510, "D1:%s\n", kb->data1));
else
write (fd, "D1!\n", 4);
if (kb->data2 && kb->data2[0])
write (fd, buf, g_snprintf (buf, 510, "D2:%s\n", kb->data2));
else
write (fd, "D2!\n", 4);
write (fd, "\n", 1);
list = g_slist_next (list);
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;
}
close (fd);
return 0;
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
+3
View File
@@ -31,6 +31,9 @@
void key_init (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_action_insert (GtkWidget * wid, GdkEventKey * evt, char *d1, char *d2,
session *sess);
-98
View File
@@ -1,98 +0,0 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "config.h"
#include <gtk/gtk.h>
#include "../common/autoaway.h"
#include "idle.h"
#ifdef WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
#include <math.h>
#elif defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
#include <gdk/gdkx.h>
#include <X11/extensions/scrnsaver.h>
#endif
/* Do not count focus changes, redraws, timers or incoming IRC traffic. */
static void
idle_event (GdkEvent *event, gpointer data)
{
switch (event->type)
{
case GDK_KEY_PRESS:
case GDK_BUTTON_PRESS:
case GDK_SCROLL:
case GDK_MOTION_NOTIFY:
case GDK_TOUCH_BEGIN:
case GDK_TOUCH_UPDATE:
autoaway_activity ();
break;
default:
break;
}
gtk_main_do_event (event);
}
void
idle_init (void)
{
autoaway_activity ();
/* Keep GTK's normal dispatch, including detached windows and dialogs. */
gdk_event_handler_set (idle_event, NULL, NULL);
}
/* Microseconds since desktop input, or -1 for the portable app fallback. */
gint64
fe_get_idle_time (void)
{
#ifdef WIN32
LASTINPUTINFO input = {0};
DWORD elapsed;
input.cbSize = sizeof input;
if (!GetLastInputInfo (&input))
return -1;
/* DWORD subtraction handles the 49-day GetTickCount wrap. Some injected
* input has a slightly newer timestamp; do not treat it as weeks idle. */
elapsed = GetTickCount () - input.dwTime;
if ((gint32)elapsed < 0)
elapsed = 0;
return (gint64)elapsed * 1000;
#elif defined(__APPLE__)
CFTimeInterval seconds = CGEventSourceSecondsSinceLastEventType (
kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType);
if (!isfinite (seconds) || seconds < 0 ||
seconds >= (double)G_MAXINT64 / G_USEC_PER_SEC)
return -1;
return (gint64)(seconds * G_USEC_PER_SEC);
#elif defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
GdkDisplay *display = gdk_display_get_default ();
XScreenSaverInfo info = {0};
Display *xdisplay;
int event_base, error_base, error;
Status ok;
const char *wayland = g_getenv ("WAYLAND_DISPLAY");
/* XWayland idle is not desktop-wide. Never pass a Wayland display to Xlib. */
if (!display || !GDK_IS_X11_DISPLAY (display) || (wayland && *wayland))
return -1;
xdisplay = gdk_x11_display_get_xdisplay (display);
gdk_x11_display_error_trap_push (display);
ok = XScreenSaverQueryExtension (xdisplay, &event_base, &error_base);
if (ok)
ok = XScreenSaverQueryInfo (xdisplay, DefaultRootWindow (xdisplay), &info);
error = gdk_x11_display_error_trap_pop (display);
if (!ok || error)
return -1;
return (gint64)info.idle * 1000;
#else
return -1;
#endif
}
-10
View File
@@ -1,10 +0,0 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef ZOITECHAT_IDLE_H
#define ZOITECHAT_IDLE_H
void idle_init (void);
#endif
+22 -18
View File
@@ -31,6 +31,7 @@
#include "../common/fe.h"
#include "gtkutil.h"
#include "maingui.h"
#include "setup.h"
#define ICON_IGNORE_NEW "document-new"
#define ICON_IGNORE_DELETE "edit-delete"
@@ -234,6 +235,8 @@ ignore_delete_entry_clicked (GtkWidget * wid, struct session *sess)
static void
ignore_store_new (int cancel, char *mask, gpointer data)
{
if (cancel || !ignorewin)
return;
GtkTreeView *view = g_object_get_data (G_OBJECT (ignorewin), "view");
GtkListStore *store = GTK_LIST_STORE (get_store ());
GtkTreeIter iter;
@@ -265,6 +268,11 @@ ignore_store_new (int cancel, char *mask, gpointer data)
static void
ignore_clear_cb (GtkDialog *dialog, gint response)
{
if (!ignorewin)
{
gtk_widget_destroy (GTK_WIDGET (dialog));
return;
}
GtkListStore *store = GTK_LIST_STORE (get_store ());
GtkTreeIter iter;
char *mask;
@@ -293,7 +301,7 @@ ignore_clear_entry_clicked (GtkWidget * wid)
{
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,
_("Are you sure you want to remove all ignores?"));
theme_manager_attach_window (dialog);
@@ -335,8 +343,8 @@ ignore_stats_entry (GtkWidget * box, char *label, int value)
return wid;
}
void
ignore_gui_open (void)
GtkWidget *
ignore_preferences_create (void)
{
GtkWidget *vbox, *box, *stat_box, *frame;
GtkWidget *view;
@@ -345,20 +353,9 @@ ignore_gui_open (void)
GSList *temp = ignore_list;
char *mask;
gboolean private, chan, notice, ctcp, dcc, invite, unignore;
char buf[128];
if (ignorewin)
{
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);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
ignorewin = vbox;
g_signal_connect (vbox, "destroy", G_CALLBACK (close_ignore_gui_callback), NULL);
view = ignore_treeview_new (vbox);
g_object_set_data (G_OBJECT (ignorewin), "view", view);
@@ -421,7 +418,14 @@ ignore_gui_open (void)
temp = temp->next;
}
gtk_widget_show (ignorewin);
gtk_widget_show_all (ignorewin);
return vbox;
}
void
ignore_gui_open (void)
{
setup_open_page ("ignore");
}
void
+69 -165
View File
@@ -50,6 +50,7 @@
#include "banlist.h"
#include "chanlist.h"
#include "editlist.h"
#include "setup.h"
#include "fkeys.h"
#ifdef __APPLE__
#include "macos-shortcuts.h"
@@ -1321,7 +1322,6 @@ menu_open_server_list (GtkWidget *wid, gpointer none)
static void
menu_settings (GtkWidget * wid, gpointer none)
{
extern void setup_open (void);
setup_open ();
}
@@ -1336,20 +1336,10 @@ menu_macos_minimize (GtkWidget *widget, gpointer userdata)
}
#endif
static void
menu_usermenu (void)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("User menu - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, usermenu_list, buf, "usermenu", "usermenu.conf", 0);
}
static void
usermenu_create (GtkWidget *menu)
{
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
@@ -1658,33 +1648,6 @@ menu_banlist (GtkWidget * wid, gpointer none)
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"\
"%c = current channel\n"\
"%e = current network name\n"\
@@ -1741,73 +1704,47 @@ menu_noplugin_info (void)
"shell instead of ZoiteChat")
static void
menu_usercommands (void)
menu_preferences_selector_changed (GtkComboBox *combo, GtkStack *stack)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("User Defined Commands - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, command_list, buf, "commands", "commands.conf",
usercommands_help);
const char *id = gtk_combo_box_get_active_id (combo);
if (id) gtk_stack_set_visible_child_name (stack, id);
}
static void
menu_ulpopup (void)
GtkWidget *
menu_preferences_create (const char *id)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("Userlist Popup menu - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, popup_list, buf, "popup", "popup.conf", ulbutton_help);
if (!strcmp (id, "replace"))
return editlist_preferences_create (&replace_list, "replace.conf", _("Text"), _("Replace with"), NULL);
if (!strcmp (id, "commands"))
return editlist_preferences_create (&command_list, "commands.conf", _("Command"), _("Expansion"), usercommands_help);
if (!strcmp (id, "urls"))
return editlist_preferences_create (&urlhandler_list, "urlhandlers.conf", _("Name"), _("Command"), url_help);
if (!strcmp (id, "ctcp"))
return editlist_preferences_create (&ctcp_list, "ctcpreply.conf", _("Request"), _("Reply"), ctcp_help);
{
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
GtkWidget *selector = gtk_combo_box_text_new ();
GtkWidget *stack = gtk_stack_new ();
const char *ids[] = {"buttons", "popup", "dlgbuttons", "usermenu"};
const char *names[] = {N_("User list buttons"), N_("User list popup"), N_("Dialog buttons"), N_("User menu")};
const char *files[] = {"buttons.conf", "popup.conf", "dlgbuttons.conf", "usermenu.conf"};
GSList **lists[] = {&button_list, &popup_list, &dlgbutton_list, &usermenu_list};
const char *help[] = {ulbutton_help, ulbutton_help, dlgbutton_help, ulbutton_help};
int i;
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);
for (i = 0; i < 4; i++)
{
gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (selector), ids[i], _(names[i]));
gtk_stack_add_named (GTK_STACK (stack), editlist_preferences_create (lists[i], files[i], _("Name"), _("Command"), help[i]), ids[i]);
}
static void
menu_rpopup (void)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("Replace - %s"), _(DISPLAY_NAME));
editlist_gui_open (_("Text"), _("Replace with"), replace_list, buf, "replace", "replace.conf", 0);
g_signal_connect (selector, "changed", G_CALLBACK (menu_preferences_selector_changed), stack);
gtk_combo_box_set_active (GTK_COMBO_BOX (selector), 0);
g_object_set_data (G_OBJECT (box), "menus-selector", selector);
g_object_set_data (G_OBJECT (stack), "preferences-selector", selector);
return box;
}
static void
menu_urlhandlers (void)
{
char buf[128];
g_snprintf(buf, sizeof(buf), _("URL Handlers - %s"), _(DISPLAY_NAME));
editlist_gui_open (NULL, NULL, urlhandler_list, buf, "urlhandlers", "urlhandlers.conf", url_help);
}
static void
menu_evtpopup (void)
{
pevent_dialog_show ();
}
static void
menu_keypopup (void)
{
key_dialog_show ();
}
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
@@ -2000,7 +1937,7 @@ menu_about (GtkWidget *wid, gpointer sess)
static struct mymenu mymenu[] = {
{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},
{N_("_New"), 0, 0, M_MENUSUB, 0, 0, 1},
@@ -2011,14 +1948,14 @@ static struct mymenu mymenu[] = {
{0, 0, 0, M_END, 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},
{0, 0, 0, M_SEP, 0, 0, 0}, /* 11 */
#define DETACH_OFFSET (12)
{0, menu_detach, 0, M_MENUITEM, 0, 0, 1}, /* 12 */
#define CLOSE_OFFSET (13)
#define DETACH_OFFSET (10)
{0, menu_detach, 0, M_MENUITEM, 0, 0, 1},
#define CLOSE_OFFSET (11)
{0, menu_close, 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}, /* 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},
#define MENUBAR_OFFSET (17)
@@ -2028,18 +1965,18 @@ static struct mymenu mymenu[] = {
{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},
{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)
{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},
{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)
{N_("Off"), menu_metres_off, 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_("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 },
{N_ ("_Fullscreen"), menu_fullscreen_toggle, 0, M_MENUTOG, MENU_ID_FULLSCREEN, 0, 1},
@@ -2052,20 +1989,7 @@ static struct mymenu mymenu[] = {
#define AWAY_OFFSET (41)
{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_("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_("_Usermenu"), 0, 0, M_NEWMENU, MENU_ID_USERMENU, 0, 1},
{N_("_Window"), 0, 0, M_NEWMENU, 0, 0, 1},
{N_("_Ban List"), menu_banlist, 0, M_MENUITEM, 0, 0, 1},
@@ -2073,9 +1997,7 @@ static struct mymenu mymenu[] = {
{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_("Friends List"), notify_opengui, 0, M_MENUITEM, 0, 0, 1},
{N_("Ignore List"), ignore_gui_open, 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_("_Raw Log"), menu_rawlog, 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},
{N_("Reset Marker Line"), menu_resetmarker, 0, M_MENUITEM, 0, 0, 1},
@@ -2083,14 +2005,14 @@ static struct mymenu mymenu[] = {
{N_("_Copy Selection"), menu_copy_selection, 0, M_MENUITEM, 0, 0, 1},
{N_("C_lear Text"), menu_flushbuffer, 0, M_MENUITEM, 0, 0, 1},
{N_("Save Text" ELLIPSIS), menu_savebuffer, 0, M_MENUITEM, 0, 0, 1},
#define SEARCH_OFFSET (71)
#define SEARCH_OFFSET (57)
{N_("Search"), 0, 0, M_MENUSUB, 0, 0, 1},
{N_("Search Text" ELLIPSIS), menu_search, 0, M_MENUITEM, 0, 0, 1},
{N_("Search Next" ), menu_search_next, 0, M_MENUITEM, 0, 0, 1},
{N_("Search Previous" ), menu_search_prev, 0, M_MENUITEM, 0, 0, 1},
{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_("_About"), menu_about, 0, M_MENUITEM, 0, 0, 1},
@@ -2100,42 +2022,24 @@ static struct mymenu mymenu[] = {
static const char *
menu_get_key_action_name (int index)
{
switch (index)
{
case 1:
return "network-list";
case 4:
return "new-server-tab";
case 6:
return "new-server-window";
case CLOSE_OFFSET:
return "close";
case 15:
return "quit";
case MENUBAR_OFFSET:
return "menu-toggle";
case MENUBAR_OFFSET + 2:
return "user-list-toggle";
case 34:
return "fullscreen-toggle";
case AWAY_OFFSET:
return "away-toggle";
case 66:
return "reset-marker";
case 67:
return "move-marker";
case 68:
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 77:
return "contents";
}
/* Bind accelerators to actions rather than positions in the menu. */
void *callback = mymenu[index].callback;
if (callback == menu_open_server_list) return "network-list";
if (callback == menu_newserver_tab) return "new-server-tab";
if (callback == menu_newserver_window) return "new-server-window";
if (callback == menu_close) return "close";
if (callback == menu_quit) return "quit";
if (callback == menu_bar_toggle_cb) return "menu-toggle";
if (callback == menu_userlist_toggle) return "user-list-toggle";
if (callback == menu_fullscreen_toggle) return "fullscreen-toggle";
if (callback == menu_away_toggle) return "away-toggle";
if (callback == menu_resetmarker) return "reset-marker";
if (callback == menu_movetomarker) return "move-marker";
if (callback == menu_copy_selection) return "copy-selection";
if (callback == menu_search) return "search-text";
if (callback == menu_search_next) return "search-next";
if (callback == menu_search_prev) return "search-previous";
if (callback == menu_docs) return "contents";
return NULL;
}
@@ -3008,7 +2912,7 @@ togitem:
#ifdef HAVE_GTK_MAC
/* 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)
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_change_layout (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);
void menu_set_away (session_gui *gui, int away);
+49 -24
View File
@@ -25,7 +25,6 @@ zoitechat_gtk_sources = [
'gtkutil.c',
'icon-resolver.c',
'ignoregui.c',
'idle.c',
'inline-image.c',
'joind.c',
'menu.c',
@@ -33,6 +32,7 @@ zoitechat_gtk_sources = [
'notifygui.c',
'pixmaps.c',
'preferences-persistence.c',
'preferences-pages.c',
'plugin-tray.c',
'plugin-notification.c',
'rawlog.c',
@@ -46,7 +46,6 @@ zoitechat_gtk_sources = [
]
zoitechat_gtk_cflags = []
idle_deps = []
zoitechat_gtk_deps = [
zoitechat_common_dep,
@@ -64,7 +63,8 @@ if host_machine.system() == 'darwin' and gtk_mac_dep.found()
gtk_dep = declare_dependency(dependencies: [gtk_dep, gtk_mac_dep])
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'
appindicator_opt = get_option('appindicator')
@@ -95,17 +95,9 @@ if host_machine.system() != 'windows'
zoitechat_gtk_deps += gdk_x11_dep
endif
xss_dep = dependency('xscrnsaver', required: false)
if gdk_x11_dep.found() and xss_dep.found()
zoitechat_gtk_deps += xss_dep
idle_deps += [gdk_x11_dep, xss_dep]
zoitechat_gtk_cflags += '-DHAVE_XSS'
endif
x11_dep = dependency('x11', required: false)
if x11_dep.found()
zoitechat_gtk_deps += x11_dep
idle_deps += x11_dep
endif
endif
@@ -115,16 +107,13 @@ zoitechat_gtk_objcflags = []
if host_machine.system() == 'windows'
zoitechat_gtk_sources += 'notifications/notification-windows.c'
zoitechat_gtk_deps += cc.find_library('dwmapi', required: true)
idle_deps += cc.find_library('user32', required: true)
zoitechat_gtk_deps += idle_deps
zoitechat_theme_deps += cc.find_library('dwmapi', required: true)
elif host_machine.system() == 'darwin'
add_languages('objc', required: true)
zoitechat_gtk_sources += 'macos-shortcuts.c'
zoitechat_gtk_sources += 'notifications/notification-osx.m'
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications', 'CoreGraphics'])
idle_deps += dependency('appleframeworks', modules: ['CoreGraphics'])
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications'])
zoitechat_gtk_objcflags += '-fobjc-arc'
else
zoitechat_gtk_sources += 'notifications/notification-freedesktop.c'
@@ -256,6 +245,50 @@ theme_runtime_tests = executable('theme_runtime_persistence_tests',
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,
protocol: 'tap',
timeout: 120,
@@ -290,7 +323,7 @@ test('Theme GTK3 Settings Tests', theme_gtk3_settings_tests,
theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests',
'theme/tests/test-theme-preferences-gtk3-populate.c',
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,
@@ -315,11 +348,3 @@ xtext_tests = executable('xtext_tests',
dependencies: [gtk_dep],
)
test('XText Tests', xtext_tests, protocol: 'tap', timeout: 120)
idle_tests = executable('idle_tests',
'tests/test-idle.c',
include_directories: [config_h_include],
dependencies: [gtk_dep] + idle_deps,
c_args: zoitechat_gtk_cflags,
)
test('Idle Backend Tests', idle_tests, protocol: 'tap', timeout: 30)
+14 -14
View File
@@ -35,6 +35,8 @@ typedef struct session zoitechat_context;
#include "../common/cfgfiles.h"
#include "gtkutil.h"
#include "maingui.h"
#include "setup.h"
#include "plugingui.h"
/* model for the plugin treeview */
enum
@@ -357,24 +359,15 @@ plugingui_reloadbutton_cb (GtkWidget *wid, GtkTreeView *view)
}
}
void
plugingui_open (void)
GtkWidget *
plugingui_preferences_create (void)
{
GtkWidget *view;
GtkWidget *view_scroll;
GtkWidget *vbox, *hbox;
char buf[128];
if (plugin_window)
{
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);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
plugin_window = vbox;
g_signal_connect (vbox, "destroy", G_CALLBACK (plugingui_close), NULL);
view = plugingui_treeview_new (vbox);
view_scroll = gtk_widget_get_parent (view);
@@ -404,4 +397,11 @@ plugingui_open (void)
fe_pluginlist_update ();
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
void plugingui_open (void);
GtkWidget *plugingui_preferences_create (void);
void plugingui_load (void);
#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 "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
preferences_persistence_save_all (void)
{
PreferencesPersistenceResult result;
char *config_temp;
char *theme_temp;
result.success = FALSE;
PreferencesPersistenceResult result = {0};
GPtrArray *files = g_ptr_array_new_with_free_func (preferences_file_free);
PreferencesFile *config = g_new0 (PreferencesFile, 1);
PreferencesFile *theme = g_new0 (PreferencesFile, 1);
guint i;
result.retry_possible = TRUE;
result.partial_failure = FALSE;
result.config_failed = FALSE;
result.theme_failed = FALSE;
result.failed_file = NULL;
config_temp = NULL;
theme_temp = NULL;
if (!save_config_prepare (&config_temp))
config->name = g_strdup ("zoitechat.conf");
theme->name = g_strdup ("colors.conf");
g_ptr_array_add (files, config);
g_ptr_array_add (files, theme);
if (!save_config_prepare (&config->temporary))
{
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";
else if (result.theme_failed)
result.failed_file = "colors.conf";
goto done;
}
save_config_discard (config_temp);
theme_runtime_save_discard (theme_temp);
g_free (config_temp);
g_free (theme_temp);
if (!theme_runtime_save_prepare (&theme->temporary))
{
result.theme_failed = TRUE;
result.failed_file = "colors.conf";
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;
}
+4
View File
@@ -34,5 +34,9 @@ typedef struct
} PreferencesPersistenceResult;
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
+16 -14
View File
@@ -70,7 +70,7 @@ static GtkWidget *entry_nick1;
static GtkWidget *entry_nick2;
static GtkWidget *entry_nick3;
static GtkWidget *entry_guser;
/* static GtkWidget *entry_greal; */
static GtkWidget *entry_greal;
enum {
SERVER_TREE,
@@ -1650,7 +1650,7 @@ servlist_savegui (void)
sp = strchr (prefs.hex_irc_user_name, ' ');
if (sp)
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 ();
if (!save_config ())
fe_message (_("Could not save zoitechat.conf."), FE_MSG_WARN);
@@ -2809,12 +2809,12 @@ servlist_open_networks (void)
GtkWidget *label4;
GtkWidget *label5;
GtkWidget *label6;
/* GtkWidget *label7; */
GtkWidget *label7;
GtkWidget *entry1;
GtkWidget *entry2;
GtkWidget *entry3;
GtkWidget *entry4;
/* GtkWidget *entry5; */
GtkWidget *entry5;
GtkWidget *vbox2;
GtkWidget *label1;
GtkWidget *table4;
@@ -2900,12 +2900,12 @@ servlist_open_networks (void)
gtk_widget_set_halign (label6, GTK_ALIGN_START);
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_table_attach (GTK_TABLE (table1), label7, 0, 1, 4, 5,
(GtkAttachOptions) (GTK_FILL),
(GtkAttachOptions) (0), 0, 0);
*/
servlist_table_attach (table1, label7, 0, 1, 4, 5,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
0, 0);
entry_nick1 = entry1 = gtk_entry_new ();
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,
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_widget_show (entry5);
gtk_table_attach (GTK_TABLE (table1), entry5, 1, 2, 4, 5,
(GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
(GtkAttachOptions) (0), 0, 0); */
servlist_table_attach (table1, entry5, 1, 2, 4, 5,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
vbox2 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
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 (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_default (button_close);
+974 -350
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -24,5 +24,9 @@
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
+4 -2
View File
@@ -928,6 +928,10 @@ sexy_spell_entry_init(SexySpellEntry *entry)
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->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)
{
@@ -939,8 +943,6 @@ sexy_spell_entry_init(SexySpellEntry *entry)
sexy_spell_entry_activate_default_languages(entry);
}
entry->priv->attr_list = pango_attr_list_new();
entry->priv->checked = TRUE;
entry->priv->parseattr = TRUE;
-85
View File
@@ -1,85 +0,0 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <gtk/gtk.h>
#include "../idle.h"
static int activity_count, dispatch_count;
static void test_gtk_dispatch (GdkEvent *event);
void
autoaway_activity (void)
{
activity_count++;
}
/* Exercise the real handler without delivering synthetic input to a widget. */
#define gtk_main_do_event test_gtk_dispatch
#include "../idle.c"
#undef gtk_main_do_event
static void
test_gtk_dispatch (GdkEvent *event)
{
dispatch_count++;
}
static void
test_user_input_and_dispatch (void)
{
GdkEvent event = {0};
GdkEventType input[] = {GDK_KEY_PRESS, GDK_BUTTON_PRESS, GDK_SCROLL,
GDK_MOTION_NOTIFY, GDK_TOUCH_BEGIN, GDK_TOUCH_UPDATE};
GdkEventType other[] = {GDK_FOCUS_CHANGE, GDK_CONFIGURE, GDK_WINDOW_STATE,
GDK_PROPERTY_NOTIFY, GDK_VISIBILITY_NOTIFY};
unsigned int i;
activity_count = dispatch_count = 0;
for (i = 0; i < G_N_ELEMENTS (input); i++)
{
event.type = input[i];
idle_event (&event, NULL);
}
g_assert_cmpint (activity_count, ==, G_N_ELEMENTS (input));
for (i = 0; i < G_N_ELEMENTS (other); i++)
{
event.type = other[i];
idle_event (&event, NULL);
}
g_assert_cmpint (activity_count, ==, G_N_ELEMENTS (input));
g_assert_cmpint (dispatch_count, ==, G_N_ELEMENTS (input) + G_N_ELEMENTS (other));
}
static void
test_native_idle (void)
{
gint64 idle = fe_get_idle_time ();
g_assert_cmpint (idle, >=, -1);
#if defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
if (gdk_display_get_default () && GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
{
/* XWayland must use the app fallback, even if XSS is compiled in. */
char *saved = g_strdup (g_getenv ("WAYLAND_DISPLAY"));
g_unsetenv ("WAYLAND_DISPLAY");
g_assert_cmpint (fe_get_idle_time (), >=, 0);
g_setenv ("WAYLAND_DISPLAY", "wayland-test", TRUE);
g_assert_cmpint (fe_get_idle_time (), ==, -1);
if (saved)
g_setenv ("WAYLAND_DISPLAY", saved, TRUE);
else
g_unsetenv ("WAYLAND_DISPLAY");
g_free (saved);
}
#endif
}
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
gtk_init_check (&argc, &argv);
g_test_add_func ("/idle/user-input-and-dispatch", test_user_input_and_dispatch);
g_test_add_func ("/idle/native-backend", test_native_idle);
return g_test_run ();
}
+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 ();
}
+65
View File
@@ -211,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");
}
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)
{
textentry *ent;
@@ -1078,6 +1142,7 @@ int main (int argc, char **argv)
ADD ("layout/font-isolation", test_font_isolation);
ADD ("layout/wide-metrics", test_wide_metrics);
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/macos-command-copy", test_macos_chat_copy);
ADD ("search/navigation-regex-clear", test_search);
+254 -63
View File
@@ -38,6 +38,8 @@
#include "theme/theme-access.h"
#include "theme/theme-manager.h"
#include "textgui.h"
#include "setup.h"
#include "preferences-persistence.h"
#define ICON_TEXTEVENT_SAVE_AS "document-save-as"
#define ICON_TEXTEVENT_OPEN "document-open"
@@ -50,6 +52,10 @@ extern char *pntevts[];
static GtkWidget *pevent_dialog = NULL, *pevent_dialog_twid,
*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"
enum
@@ -148,10 +154,17 @@ PrintTextRaw (void *xtbuf, unsigned char *text, int indent, time_t stamp)
}
static void
pevent_dialog_close (GtkWidget *wid, gpointer arg)
pevent_preferences_destroy (GtkWidget *widget, gpointer data)
{
pevent_dialog = NULL;
pevent_save (NULL);
int i;
(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
@@ -211,9 +224,14 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo
char *out;
int sig;
if (!gtkutil_treeview_get_selected (GTK_TREE_VIEW (pevent_dialog_list),
&iter, ROW_COLUMN, &sig, -1))
path = gtk_tree_path_new_from_string (pathstr);
if (!gtk_tree_model_get_iter (model, &iter, path))
{
gtk_tree_path_free (path);
return;
}
gtk_tree_path_free (path);
gtk_tree_model_get (model, &iter, ROW_COLUMN, &sig, -1);
text = new_text;
len = strlen (new_text);
@@ -234,16 +252,11 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo
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_tree_path_free (path);
g_free (pntevts_text[sig]);
g_free (pntevts[sig]);
pntevts_text[sig] = g_strdup (text);
pntevts[sig] = out;
g_free (pevent_staged[sig]);
pevent_staged[sig] = g_strdup (text);
g_free (out);
out = g_malloc (len + 2);
memcpy (out, text, len + 1);
@@ -257,8 +270,7 @@ pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpo
/* Scroll to bottom */
gtk_adjustment_set_value (xtext->adj, gtk_adjustment_get_upper (xtext->adj));
/* save this when we exit */
prefs.save_pevents = 1;
/* The Preferences Save action commits this staged template. */
}
static void
@@ -316,54 +328,223 @@ pevent_dialog_fill (GtkWidget *list)
i--;
gtk_list_store_insert_with_values (store, &iter, 0,
EVENT_COLUMN, te[i].name,
TEXT_COLUMN, pntevts_text[i],
TEXT_COLUMN, pevent_staged[i],
ROW_COLUMN, i, -1);
}
while (i != 0);
}
static void
pevent_save_req_cb (void *arg1, char *file)
static char *
pevent_preferences_serialize (void)
{
if (file)
pevent_save (file);
GString *text = g_string_new ("");
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
pevent_save_cb (GtkWidget * wid, void *data)
static gboolean
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,
NULL, NULL, FRF_WRITE);
return;
g_free (compiled);
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Invalid message format for %s."), te[event].name);
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
pevent_load_req_cb (void *arg1, char *file)
gboolean
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);
pevent_make_pntevts ();
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, _("Open message formatting before importing templates."));
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);
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
pevent_file_open (GtkWidget *button, gpointer exporting)
{
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));
}
gboolean
pevent_preferences_stage (GError **error)
{
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
pevent_load_cb (GtkWidget * wid, void *data)
{
gtkutil_file_req (NULL, _("Print Texts File"), pevent_load_req_cb, NULL, NULL, NULL, 0);
}
static void
pevent_ok_cb (GtkWidget * wid, void *data)
{
gtk_widget_destroy (pevent_dialog);
}
static void
pevent_test_cb (GtkWidget * wid, GtkWidget * twid)
@@ -373,7 +554,7 @@ pevent_test_cb (GtkWidget * wid, GtkWidget * twid)
for (n = 0; n < NUM_XP; n++)
{
text = _(pntevts_text[n]);
text = pevent_staged[n];
len = strlen (text);
out = g_malloc (len + 2);
@@ -485,22 +666,27 @@ pevent_hlist_treeview_new (GtkWidget *box)
return view;
}
void
pevent_dialog_show (void)
static 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;
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);
return;
pevent_original[i] = g_strdup (pntevts_text[i]);
pevent_staged[i] = g_strdup (pntevts_text[i]);
}
pevent_dialog =
mg_create_generic_tab ("edit events", _("Edit Events"),
TRUE, FALSE, pevent_dialog_close, NULL,
600, 455, &vbox, 0);
g_signal_connect (vbox, "destroy", G_CALLBACK (pevent_preferences_destroy), NULL);
preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
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);
gtk_widget_set_sensitive (pevent_dialog_twid, FALSE);
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), PEVENT_THEME_LISTENER_ID_KEY,
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);
gtk_button_box_set_layout (GTK_BUTTON_BOX (hbox), GTK_BUTTONBOX_SPREAD);
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 4);
gtkutil_button (hbox, ICON_TEXTEVENT_SAVE_AS, NULL, pevent_save_cb,
(void *) 1, _("Save As..."));
gtkutil_button (hbox, ICON_TEXTEVENT_OPEN, NULL, pevent_load_cb,
NULL, _("Load From..."));
gtkutil_button (hbox, ICON_TEXTEVENT_SAVE_AS, NULL, pevent_file_open,
(void *) 1, _("Export..."));
gtkutil_button (hbox, ICON_TEXTEVENT_OPEN, NULL, pevent_file_open,
NULL, _("Import..."));
gtkutil_button (hbox, NULL, NULL, pevent_test_cb,
pevent_dialog_twid, _("Test All"));
gtkutil_button (hbox, ICON_TEXTEVENT_OK, NULL, pevent_ok_cb,
NULL, _("OK"));
gtk_widget_show_all (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 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
@@ -112,6 +112,14 @@ theme_runtime_mode_has_user_colors (gboolean dark_mode)
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
theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
{
@@ -571,6 +571,31 @@ test_stale_theme_symlink_is_replaced (void)
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
main (int argc, char **argv)
{
@@ -596,6 +621,7 @@ main (int argc, char **argv)
if (!gtk_available)
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 ();
theme_gtk3_disable ();
teardown_themes ();
@@ -25,6 +25,7 @@
#include "../../../common/gtk3-theme-service.h"
#include "../theme-gtk3.h"
#include "../theme-manager.h"
#include "../theme-runtime.h"
struct session *current_sess;
struct session *current_tab;
@@ -38,6 +39,61 @@ static char applied_theme_id[256];
static ThemeGtk3Variant applied_variant;
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 *
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)color;
set_token_color_calls++;
theme_runtime_user_set_color (token, color);
if (changed)
*changed = FALSE;
}
@@ -458,6 +515,154 @@ test_select_theme_does_not_pin_palette_tokens (void)
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
main (int argc, char **argv)
{
@@ -471,5 +676,10 @@ main (int argc, char **argv)
test_select_none_resets_theme_and_applies);
g_test_add_func ("/theme/preferences/gtk3_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 ();
}
@@ -430,6 +430,36 @@ test_save_writes_only_custom_token_keys (void)
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
main (int argc, char **argv)
{
@@ -450,5 +480,6 @@ main (int argc, char **argv)
test_save_discard_unlinks_temp_file);
g_test_add_func ("/theme/runtime/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 ();
}
+8
View File
@@ -206,6 +206,14 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map
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
theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
{
+1
View File
@@ -28,6 +28,7 @@
#include "theme-palette.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_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba);
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
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])
{
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;
}
+3
View File
@@ -24,6 +24,9 @@
#include <glib.h>
#include <gdk/gdk.h>
#define THEME_GTK3_BUILTIN_LIGHT "builtin:light"
#define THEME_GTK3_BUILTIN_DARK "builtin:dark"
typedef enum
{
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_commit (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
+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);
}
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
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);
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed);
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_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col);
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;
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;
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);
}
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))
{
char *time_str;
-6
View File
@@ -48,12 +48,6 @@
#endif
gint64
fe_get_idle_time (void)
{
return -1;
}
static int done = FALSE;
-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{EE388A27-6992-43C0-BA9B-61D8968D42E3}</ProjectGuid>
<SolutionDir>$(MSBuildThisFileDirectory)</SolutionDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="zoitechat.props" />
<PropertyGroup>
<TargetName>autoaway-tests</TargetName>
<OutDir>$(ZoiteChatBuild)\$(ZoiteChatPlatform)\tests\</OutDir>
<IntDir>$(ZoiteChatObj)autoaway-tests\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>WIN32;_WIN64;_CONSOLE;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>gio-2.0.lib;gobject-2.0.lib;glib-2.0.lib;$(IntlLib);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\src\common\autoaway.c" />
<ClCompile Include="..\src\common\tests\test-autoaway.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>