15 Commits

Author SHA1 Message Date
b0a5c6f5e5 Improve GTK3 Performance 2026-09-09 16:45:28 -06:00
bb3893d36c Fix channel ACTION routing after self-message handling 2026-09-03 09:27:07 -06:00
deepend-tildeclub
fe57449f6c Merge pull request #374 from TehPeGaSuS/fix/weechat-relay-pm-split
Fix private messages splitting into two windows over WeeChat's IRC relay
2026-09-03 08:20:55 -07:00
ThePeGaSuS
c725adaa09 Fix CTCP ACTION not rerouting self-sent /me to correct PM window
Mirrors the PRIVMSG fix: replayed /me messages sent by our own nick
via WeeChat relay backlog were always passed with fromme=FALSE,
so they never got rerouted to the actual PM query window.
2026-09-03 08:51:21 +02:00
deepend-tildeclub
7731bfa4e0 Merge pull request #377 from ZoiteChat/retain-searched-text-view
unblock navigation when in search
2026-09-02 09:46:13 -07:00
e72546aaec fix accidental removal of mg_apply_emoji_fallback_widget (entry); 2026-09-02 10:43:09 -06:00
d6d648f0f4 unblock navigation when in search 2026-09-02 10:41:21 -06:00
deepend-tildeclub
56fddd56f5 Merge pull request #340 from bgermann/winspell
Require Enchant 2.8.19+ and drop win8 provider
2026-09-01 15:06:11 -07:00
Bastian Germann
3c9c350bc0 win32: Set Enchant prefix dir to install location
Enchant looks for its providers (including the WinSpell spellcheck
provider) under a hardcoded default path derived from its build
prefix, which is C:\gtk-build\gtk\x64\release\lib\enchant-2 when
built via gvsbuild. After installation, the provider DLLs live
under ZoiteChat's own install directory, so Enchant fails to find
them and spellcheck silently stops working.

Call enchant_set_prefix_dir() to redirect Enchant's provider search
to <install_dir> before the first enchant_broker_init().
enchant_set_prefix_dir() expects the installation prefix itself;
Enchant adds lib\enchant-2 when searching for providers.
The symbol is loaded optionally since it may not exist in older
Enchant builds; if missing, behavior is unchanged.
2026-09-01 21:36:40 +02:00
Bastian Germann
8340a04bd6 win32: Use parentheses to be independent of and/or evaluation order
The Version.NTPlatform shall be true for every evaluation of the if
branch.
2026-08-31 14:45:31 +02:00
Bastian Germann
ccef70fd3d Clarify comments about spelling provider 2026-08-31 14:29:16 +02:00
Bastian Germann
0650fa7bbe win32: Reference enchant-2 in installer 2026-08-31 14:17:58 +02:00
Bastian Germann
5cffa06385 Update gvsbuild to version with updated enchant 2026-08-31 14:17:58 +02:00
Bastian Germann
4df17a151e Require Enchant 2.8.19+ and drop win8 provider
Require Enchant 2.8.19 or newer so Windows can use upstream winspell.
Remove the in-tree libenchant_win8 provider project and sources.
Update runtime library probing to prefer Enchant 2 library names.
2026-08-31 14:17:58 +02:00
ThePeGaSuS
0ba523d071 Fix: private messages split into two windows over WeeChat's IRC relay
When you send a private message, ZoiteChat sometimes gets the same
message echoed back by the server so it can show it in the right
window. Whether it recognizes that echo depended on the server first
confirming a feature called "echo-message" during connection setup.

WeeChat's relay doesn't seem to confirm that feature the way other
servers (or ZNC) do, so ZoiteChat didn't recognize its own echoed
messages and treated them as brand new incoming messages from
yourself, opening a second, separate conversation window instead of
using the one you were already typing in.

This makes ZoiteChat recognize its own message by checking who sent
it, instead of only trusting that feature confirmation. That way,
sent and received messages always end up in the same conversation
window, regardless of how well the server/relay announces its
capabilities.
2026-08-28 20:48:12 +02:00
18 changed files with 106 additions and 487 deletions

View File

@@ -66,7 +66,7 @@ jobs:
Download-WithRetry -Url https://github.com/jrsoftware/issrc/releases/download/is-6_7_1/innosetup-6.7.1.exe -OutFile deps\innosetup-unicode.exe
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.1/GTK3_Gvsbuild_zoitechat-2.18.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK3_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
Expand-Archive -LiteralPath deps\gtk-${{ matrix.arch }}.zip -DestinationPath C:\gtk-build\gtk\x64\release -Force
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst

View File

@@ -591,6 +591,7 @@ const struct prefs vars[] =
{"text_replay", P_OFFINT (hex_text_replay), TYPE_BOOL},
{"text_search_case_match", P_OFFINT (hex_text_search_case_match), TYPE_BOOL},
{"text_search_highlight_all", P_OFFINT (hex_text_search_highlight_all), TYPE_BOOL},
{"text_search_keep_position", P_OFFINT (hex_text_search_keep_position), TYPE_BOOL},
{"text_search_follow", P_OFFINT (hex_text_search_follow), TYPE_BOOL},
{"text_search_regexp", P_OFFINT (hex_text_search_regexp), TYPE_BOOL},
{"text_show_marker", P_OFFINT (hex_text_show_marker), TYPE_BOOL},

View File

@@ -126,7 +126,21 @@ ctcp_handle (session *sess, char *to, char *nick, char *ip,
if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset))
goto generic;
inbound_action (sess, to, nick, ip, msg + 7, FALSE, tags_data->identified, tags_data);
{
gboolean private_fromme = !serv->p_cmp (nick, serv->nick) && !is_channel (serv, to);
if (private_fromme)
{
session *target = find_dialog (serv, to);
if (target)
sess = target;
else if (serv->front_session)
sess = serv->front_session;
}
inbound_action (sess, to, nick, ip, msg + 7, private_fromme, tags_data->identified, tags_data);
}
return;
}

View File

@@ -1368,7 +1368,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
{
if (ignore_check (word[1], IG_PRIV))
return;
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick))
if (!serv->p_cmp (nick, serv->nick))
{
session *target_sess = find_dialog (serv, to);

View File

@@ -217,6 +217,7 @@ struct zoitechatprefs
unsigned int hex_text_replay;
unsigned int hex_text_search_case_match;
unsigned int hex_text_search_highlight_all;
unsigned int hex_text_search_keep_position;
unsigned int hex_text_search_follow;
unsigned int hex_text_search_regexp;
unsigned int hex_text_show_marker;

View File

@@ -114,7 +114,7 @@ typedef struct restore_gui
/* information stored when this tab isn't front-most */
GtkListStore *user_model; /* for filling the GtkTreeView */
GHashTable *user_row_refs;
GHashTable *user_row_iters; /* User * -> persistent GtkTreeIter * */
void *buffer; /* xtext_Buffer */
char *input_text; /* input text buffer (while not-front tab) */
char *topic_text; /* topic GtkEntry buffer */

View File

@@ -4688,7 +4688,11 @@ mg_search_toggle(session *sess)
{
gtk_widget_hide(sess->gui->shbox);
gtk_widget_grab_focus(sess->gui->input_box);
if (prefs.hex_text_search_keep_position)
g_signal_handler_block (sess->gui->shentry, sess->gui->search_changed_signal);
gtk_entry_set_text(GTK_ENTRY(sess->gui->shentry), "");
if (prefs.hex_text_search_keep_position)
g_signal_handler_unblock (sess->gui->shentry, sess->gui->search_changed_signal);
}
else
{
@@ -4702,10 +4706,19 @@ mg_search_toggle(session *sess)
}
static gboolean
search_handle_esc (GtkWidget *win, GdkEventKey *key, session *sess)
search_handle_keypress (GtkWidget *win, GdkEventKey *key, session *sess)
{
if (key->keyval == GDK_KEY_Escape)
{
mg_search_toggle(sess);
return TRUE;
}
if (((key->keyval == GDK_KEY_Page_Up || key->keyval == GDK_KEY_Page_Down) &&
!(key->state & gtk_accelerator_get_default_mod_mask ())) ||
((key->keyval == GDK_KEY_Up || key->keyval == GDK_KEY_Down) &&
(key->state & gtk_accelerator_get_default_mod_mask ()) == GDK_SHIFT_MASK))
return key_handle_key_press (sess->gui->input_box, key, sess);
return FALSE;
}
@@ -4735,7 +4748,7 @@ mg_create_search(session *sess, GtkWidget *box)
mg_apply_emoji_fallback_widget (entry);
mg_apply_entry_scroll_artifact_fix (entry);
gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess);
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_esc), sess);
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_keypress), sess);
g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(mg_search_handle_next), sess);
gtk_entry_set_icon_activatable (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY, FALSE);
gtk_entry_set_icon_tooltip_text (GTK_ENTRY (sess->gui->shentry), GTK_ENTRY_ICON_SECONDARY, _("Search hit end or not found."));
@@ -5773,9 +5786,9 @@ void
fe_session_callback (session *sess)
{
gtk_xtext_buffer_free (sess->res->buffer);
if (sess->res->user_row_iters)
g_hash_table_destroy (sess->res->user_row_iters);
g_object_unref (G_OBJECT (sess->res->user_model));
if (sess->res->user_row_refs)
g_hash_table_destroy (sess->res->user_row_refs);
if (sess->res->banlist && sess->res->banlist->window)
mg_close_gen (NULL, sess->res->banlist->window);

View File

@@ -171,6 +171,7 @@ static const setting appearance_settings[] =
{ST_TOGGLE, N_("Colored nick names"), P_OFFINTNL(hex_text_color_nicks), N_("Give each person on IRC a different color"),0,0},
{ST_TOGGLR, N_("Indent nick names"), P_OFFINTNL(hex_text_indent), N_("Make nick names right-justified"),0,0},
{ST_TOGGLE, N_ ("Show marker line"), P_OFFINTNL (hex_text_show_marker), N_ ("Insert a red line after the last read text."), 0, 0},
{ST_TOGGLE, N_("Keep search position when closing"), P_OFFINTNL(hex_text_search_keep_position), 0, 0, 0},
{ST_HEADER, N_("Timestamps"),0,0,0},
{ST_TOGGLE, N_("Enable timestamps"), P_OFFINTNL(hex_stamp_text),0,0,1},
@@ -219,7 +220,7 @@ static const setting inputbox_settings[] =
{ST_TOGGLE, N_("Spell checking"), P_OFFINTNL(hex_gui_input_spell),0,0,1},
{ST_ENTRY, N_("Dictionaries to use:"), P_OFFSETNL(hex_text_spell_langs),0,0,sizeof prefs.hex_text_spell_langs},
#ifdef WIN32
{ST_LABEL, N_("Use language codes (as in \"%LOCALAPPDATA%\\enchant\\myspell\\dicts\").\nSeparate multiple entries with commas.")},
{ST_LABEL, N_("Use language codes (for example, \"en_US\").\nOn Windows 8 and newer, dictionaries are provided by Windows. Separate multiple entries with commas.")},
#else
{ST_LABEL, N_("Use language codes. Separate multiple entries with commas.")},
#endif

View File

@@ -113,6 +113,9 @@ static void (*enchant_dict_describe) (struct EnchantDict * dict, EnchantDictDesc
static void (*enchant_dict_free_suggestions) (struct EnchantDict * dict, char **suggestions);
static void (*enchant_dict_store_replacement) (struct EnchantDict * dict, const char *const mis, ssize_t mis_len, const char *const cor, ssize_t cor_len);
static char ** (*enchant_dict_suggest) (struct EnchantDict * dict, const char *const word, ssize_t len, size_t * out_n_suggs);
#ifdef G_OS_WIN32
static void (*enchant_set_prefix_dir) (const char *dir);
#endif
static gboolean have_enchant = FALSE;
struct _SexySpellEntryPriv
@@ -198,14 +201,16 @@ initialize_enchant (void)
gsize i;
const char * const libnames[] = {
#ifdef G_OS_WIN32
"libenchant-2.dll",
"libenchant-2-2.dll",
"libenchant.dll",
#endif
#ifdef G_OS_UNIX
"libenchant.so.1",
"libenchant.so.2",
"libenchant-2.so.2",
#endif
#ifdef __APPLE__
"libenchant-2.dylib",
"libenchant.dylib",
#endif
};
@@ -255,6 +260,23 @@ initialize_enchant (void)
MODULE_SYMBOL("enchant_dict_store_replacement",
enchant_dict_store_replacement, NULL)
MODULE_SYMBOL("enchant_dict_suggest", enchant_dict_suggest, NULL)
#ifdef G_OS_WIN32
if (g_module_symbol(enchant, "enchant_set_prefix_dir", &funcptr))
{
gchar *install_dir;
enchant_set_prefix_dir = funcptr;
install_dir = g_win32_get_package_installation_directory_of_module(NULL);
if (install_dir)
{
enchant_set_prefix_dir(install_dir);
g_info("Enchant prefix dir set to %s", install_dir);
g_free(install_dir);
}
}
#endif
}
static void

View File

@@ -218,75 +218,49 @@ scroll_to_iter (GtkTreeIter *iter, GtkTreeView *treeview, GtkTreeModel *model)
static GHashTable *
userlist_row_map_ensure (session *sess)
{
if (!sess->res->user_row_refs)
sess->res->user_row_refs = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) gtk_tree_row_reference_free);
if (!sess->res->user_row_iters)
sess->res->user_row_iters = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) gtk_tree_iter_free);
return sess->res->user_row_refs;
return sess->res->user_row_iters;
}
static void
userlist_row_map_remove (session *sess, struct User *user)
{
if (!sess->res->user_row_refs)
if (!sess->res->user_row_iters)
return;
g_hash_table_remove (sess->res->user_row_refs, user);
g_hash_table_remove (sess->res->user_row_iters, user);
}
static void
userlist_row_map_set (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter)
{
GtkTreePath *path;
GtkTreeRowReference *ref;
path = gtk_tree_model_get_path (model, iter);
if (!path)
/* The shared tree view can still show another session's model while a
* tab switch is pending. Never cache an iterator from that model. */
if (model != GTK_TREE_MODEL (sess->res->user_model))
return;
ref = gtk_tree_row_reference_new (model, path);
gtk_tree_path_free (path);
if (!ref)
return;
g_hash_table_replace (userlist_row_map_ensure (sess), user, ref);
/* GtkListStore guarantees persistent iterators until their row is
* removed, including across sorting. Unlike row references, these do
* not require every cached position to be updated on each insertion. */
g_hash_table_replace (userlist_row_map_ensure (sess), user, gtk_tree_iter_copy (iter));
}
static gboolean
userlist_row_map_get_iter (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter)
{
GtkTreeRowReference *ref;
GtkTreePath *path;
struct User *row_user;
GtkTreeIter *cached;
if (!sess->res->user_row_refs)
if (model != GTK_TREE_MODEL (sess->res->user_model) || !sess->res->user_row_iters)
return FALSE;
ref = g_hash_table_lookup (sess->res->user_row_refs, user);
if (!ref)
cached = g_hash_table_lookup (sess->res->user_row_iters, user);
if (!cached)
return FALSE;
path = gtk_tree_row_reference_get_path (ref);
if (!path)
{
g_hash_table_remove (sess->res->user_row_refs, user);
return FALSE;
}
if (!gtk_tree_model_get_iter (model, iter, path))
{
gtk_tree_path_free (path);
g_hash_table_remove (sess->res->user_row_refs, user);
return FALSE;
}
gtk_tree_path_free (path);
gtk_tree_model_get (model, iter, COL_USER, &row_user, -1);
if (row_user != user)
{
g_hash_table_remove (sess->res->user_row_refs, user);
return FALSE;
}
/* Removal and clear invalidate the cache before deleting model rows. */
*iter = *cached;
return TRUE;
}
@@ -587,7 +561,6 @@ fe_userlist_rehash (session *sess, struct User *user)
GTK_TREE_MODEL(sess->res->user_model), user, &sel);
if (!iter)
return;
userlist_row_map_set (sess, GTK_TREE_MODEL (sess->res->user_model), user, iter);
if (prefs.hex_away_track && user->away)
{
@@ -698,8 +671,8 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
void
fe_userlist_clear (session *sess)
{
if (sess->res->user_row_refs)
g_hash_table_remove_all (sess->res->user_row_refs);
if (sess->res->user_row_iters)
g_hash_table_remove_all (sess->res->user_row_iters);
gtk_list_store_clear (sess->res->user_model);
}

View File

@@ -3130,6 +3130,7 @@ gtk_xtext_class_init (GtkXTextClass * class)
typedef struct chunk_s {
GSList *slp;
gboolean collect_metadata;
int off1, len1, emph;
offlen_t meta;
} chunk_t;
@@ -3142,12 +3143,19 @@ xtext_do_chunk(chunk_t *c)
if (c->len1 == 0)
return;
/* Copying, searching and saving only need the stripped text. */
if (!c->collect_metadata)
{
c->len1 = 0;
return;
}
meta = g_new (offlen_t, 1);
meta->off = c->off1;
meta->len = c->len1;
meta->emph = c->emph;
meta->width = 0;
c->slp = g_slist_append (c->slp, meta);
c->slp = g_slist_prepend (c->slp, meta);
c->len1 = 0;
}
@@ -3170,6 +3178,7 @@ gtk_xtext_strip_color (unsigned char *text, int len, unsigned char *outbuf,
new_str = outbuf;
c.slp = NULL;
c.collect_metadata = slpp != NULL;
c.off1 = 0;
c.len1 = 0;
c.emph = 0;
@@ -3240,9 +3249,7 @@ bad_utf8: /* Normal ending sequence, and give up if bad utf8 */
*newlen = i;
if (slpp)
*slpp = c.slp;
else
g_slist_free_full (c.slp, g_free);
*slpp = g_slist_reverse (c.slp);
return new_str;
}
@@ -4341,7 +4348,7 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
if (win_width >= ent->indent + ent->str_width)
{
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len));
ent->sublines = g_slist_prepend (ent->sublines, GINT_TO_POINTER (ent->str_len));
ent->subline_count = 1;
return ent->subline_count;
}
@@ -4352,13 +4359,15 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
do
{
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
ent->sublines = g_slist_prepend (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
ent->subline_count++;
indent = buf->indent;
str += len;
}
while (str < ent->str + ent->str_len);
ent->subline_count = g_slist_length (ent->sublines);
/* Preserve display order without walking the growing list per wrap. */
ent->sublines = g_slist_reverse (ent->sublines);
return ent->subline_count;
}

View File

@@ -1,2 +0,0 @@
EXPORTS
init_enchant_provider

View File

@@ -1,43 +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>{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>libenchant_win8</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="..\..\win32\zoitechat.props" />
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup>
<OutDir>$(ZoiteChatRel)lib\enchant\</OutDir>
<TargetName>libenchant_win8</TargetName>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\common;$(ZoiteChatLib);$(DepsRoot)\include\enchant;$(DepsRoot)\include;$(OpenSslInclude);$(Glib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<ModuleDefinitionFile>libenchant_win8.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="win8_provider.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="win8_provider.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -1,340 +0,0 @@
/* ZoiteChat
* Copyright (c) 2015 Patrick Griffis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "config.h"
#include <Spellcheck.h>
#include <windows.h>
#include <cstdlib>
#include <climits>
#include "typedef.h" // for ssize_t
#include <enchant-provider.h>
ENCHANT_PLUGIN_DECLARE ("win8")
static int
size_to_int (size_t value)
{
return value > static_cast<size_t>(INT_MAX) ? INT_MAX : static_cast<int>(value);
}
static char *
utf16_to_utf8 (const wchar_t * const str, bool from_bcp47)
{
if (!str)
return nullptr;
int needed = WideCharToMultiByte (CP_UTF8, 0, str, -1, nullptr, 0, nullptr, nullptr);
if (needed <= 0)
return nullptr;
char *utf8 = static_cast<char*>(std::malloc (static_cast<size_t>(needed)));
if (!utf8)
return nullptr;
if (WideCharToMultiByte (CP_UTF8, 0, str, -1, utf8, needed, nullptr, nullptr) != needed)
{
std::free (utf8);
return nullptr;
}
if (utf8 && from_bcp47)
{
char *p = utf8;
/* bcp47 tags use syntax "en-US" while the myspell versions are "en_US" */
while (*p)
{
if (*p == '-')
*p = '_';
p++;
}
}
return utf8;
}
static wchar_t *
utf8_to_utf16 (const char * const str, int len, bool to_bcp47)
{
if (!str)
return nullptr;
int needed = MultiByteToWideChar (CP_UTF8, 0, str, len, nullptr, 0);
if (needed <= 0)
return nullptr;
int alloc_len = (len == -1) ? needed : needed + 1;
wchar_t *utf16 = static_cast<wchar_t*>(std::malloc (sizeof (wchar_t) * static_cast<size_t>(alloc_len)));
if (!utf16)
return nullptr;
if (MultiByteToWideChar (CP_UTF8, 0, str, len, utf16, needed) != needed)
{
std::free (utf16);
return nullptr;
}
if (len != -1)
utf16[needed] = L'\0';
if (utf16 && to_bcp47)
{
wchar_t *p = utf16;
/* bcp47 tags use syntax "en-US" while the myspell versions are "en_US" */
while (*p)
{
if (*p == L'_')
*p = L'-';
p++;
}
}
return utf16;
}
static char **
enumstring_to_chararray (IEnumString *strings, size_t *out_len, bool from_bcp47)
{
char **chars = static_cast<char**>(std::calloc (256, sizeof (char*)));
LPOLESTR wstr = nullptr;
size_t i = 0;
if (!chars)
{
*out_len = 0;
strings->Release ();
return nullptr;
}
while (SUCCEEDED (strings->Next (1, &wstr, nullptr)) && i < 256 && wstr)
{
char *str = utf16_to_utf8 (wstr, from_bcp47);
if (str)
{
chars[i] = str;
i++;
}
CoTaskMemFree (wstr);
}
chars[i] = nullptr;
strings->Release ();
*out_len = i;
return chars;
}
static void
win8_dict_add_to_personal (EnchantDict *dict, const char *const word, size_t len)
{
auto checker = static_cast<ISpellChecker*>(dict->user_data);
wchar_t *wword = utf8_to_utf16 (word, size_to_int (len), false);
checker->Add (wword);
std::free (wword);
}
static void
win8_dict_add_to_session (EnchantDict *dict, const char *const word, size_t len)
{
auto checker = static_cast<ISpellChecker*>(dict->user_data);
wchar_t *wword = utf8_to_utf16 (word, size_to_int (len), false);
checker->Ignore (wword);
std::free (wword);
}
static int
win8_dict_check (EnchantDict *dict, const char *const word, size_t len)
{
auto checker = static_cast<ISpellChecker*>(dict->user_data);
wchar_t *wword = utf8_to_utf16 (word, size_to_int (len), false);
IEnumSpellingError *errors;
ISpellingError *error = nullptr;
HRESULT hr;
hr = checker->Check (wword, &errors);
std::free (wword);
if (FAILED (hr))
return -1;
if (errors->Next (&error) == S_OK)
{
error->Release ();
errors->Release ();
return 1;
}
else
{
errors->Release ();
return 0;
}
}
static char **
win8_dict_suggest (EnchantDict *dict, const char *const word, size_t len, size_t *out_n_suggs)
{
auto checker = static_cast<ISpellChecker*>(dict->user_data);
wchar_t *wword = utf8_to_utf16 (word, size_to_int (len), false);
IEnumString *suggestions;
HRESULT hr;
hr = checker->Suggest (wword, &suggestions);
std::free (wword);
if (FAILED (hr))
{
*out_n_suggs = 0;
return nullptr;
}
return enumstring_to_chararray (suggestions, out_n_suggs, false);
}
static EnchantDict *
win8_provider_request_dict (EnchantProvider *provider, const char *const tag)
{
auto factory = static_cast<ISpellCheckerFactory*>(provider->user_data);
ISpellChecker *checker;
EnchantDict *dict;
wchar_t *wtag = utf8_to_utf16 (tag, -1, true);
HRESULT hr;
hr = factory->CreateSpellChecker (wtag, &checker);
std::free (wtag);
if (FAILED (hr))
return nullptr;
dict = static_cast<EnchantDict*>(std::calloc (1, sizeof (EnchantDict)));
dict->suggest = win8_dict_suggest;
dict->check = win8_dict_check;
dict->add_to_personal = win8_dict_add_to_personal;
dict->add_to_exclude = win8_dict_add_to_personal;
dict->add_to_session = win8_dict_add_to_session;
dict->user_data = checker;
return dict;
}
static void
win8_provider_dispose_dict (EnchantProvider *provider, EnchantDict *dict)
{
if (dict)
{
auto checker = static_cast<ISpellChecker*>(dict->user_data);
checker->Release ();
std::free (dict);
}
}
static int
win8_provider_dictionary_exists (EnchantProvider *provider, const char *const tag)
{
auto factory = static_cast<ISpellCheckerFactory*>(provider->user_data);
wchar_t *wtag = utf8_to_utf16 (tag, -1, true);
BOOL is_supported = FALSE;
factory->IsSupported (wtag, &is_supported);
std::free (wtag);
return is_supported;
}
static char **
win8_provider_list_dicts (EnchantProvider *provider, size_t *out_n_dicts)
{
auto factory = static_cast<ISpellCheckerFactory*>(provider->user_data);
IEnumString *dicts;
if (FAILED(factory->get_SupportedLanguages (&dicts)))
{
*out_n_dicts = 0;
return nullptr;
}
return enumstring_to_chararray (dicts, out_n_dicts, true);
}
static void
win8_provider_free_string_list (EnchantProvider *provider, char **str_list)
{
if (!str_list)
return;
for (size_t i = 0; str_list[i]; ++i)
std::free (str_list[i]);
std::free (str_list);
}
static void
win8_provider_dispose (EnchantProvider *provider)
{
if (provider)
{
auto factory = static_cast<ISpellCheckerFactory*>(provider->user_data);
factory->Release();
std::free (provider);
}
}
static const char *
win8_provider_identify (EnchantProvider *provider)
{
return "win8";
}
static const char *
win8_provider_describe (EnchantProvider *provider)
{
return "Windows 8 SpellCheck Provider";
}
extern "C"
{
EnchantProvider *
init_enchant_provider (void)
{
EnchantProvider *provider;
ISpellCheckerFactory *factory;
if (FAILED (CoCreateInstance (__uuidof(SpellCheckerFactory), nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS (&factory))))
return nullptr;
provider = static_cast<EnchantProvider*>(std::calloc (1, sizeof (EnchantProvider)));
provider->dispose = win8_provider_dispose;
provider->request_dict = win8_provider_request_dict;
provider->dispose_dict = win8_provider_dispose_dict;
provider->dictionary_exists = win8_provider_dictionary_exists;
provider->identify = win8_provider_identify;
provider->describe = win8_provider_describe;
provider->list_dicts = win8_provider_list_dicts;
provider->free_string_list = win8_provider_free_string_list;
provider->user_data = factory;
return provider;
}
}

View File

@@ -73,7 +73,7 @@
<LuaShare Include="$(LuaShareDir)\**\*.lua" Condition="'$(LuaEnabled)'=='true' and '$(LuaShareDir)'!=''" />
<Typelib Include="$(DepsRoot)\lib\girepository-1.0\*.typelib" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" />
<EnchantProviders Include="$(DepsRoot)\lib\enchant\*.dll" />
<EnchantProviders Include="$(DepsRoot)\lib\enchant-2\*.dll" />
<Gtk3Immodules Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\immodules\**\*" />
<Gtk3PrintBackends Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\printbackends\**\*" />
@@ -99,7 +99,7 @@
<Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" />
<Copy SourceFiles="$(WinSparklePath)\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\WinSparkle" />
<Copy SourceFiles="@(EnchantProviders)" DestinationFolder="$(ZoiteChatRel)\lib\enchant" />
<Copy SourceFiles="@(EnchantProviders)" DestinationFolder="$(ZoiteChatRel)\lib\enchant-2" />
<Copy SourceFiles="@(Locale)" DestinationFiles="@(Locale->'$(ZoiteChatRel)\share\locale\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(MSWindowsTheme)" DestinationFiles="@(MSWindowsTheme->'$(ZoiteChatRel)\share\themes\MS-Windows\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(HicolorIcons)" DestinationFiles="@(HicolorIcons->'$(ZoiteChatRel)\share\icons\hicolor\%(RecursiveDir)%(Filename)%(Extension)')" />

View File

@@ -49,7 +49,7 @@ Name: "icons"; Description: "Create Shortcuts"; Types: custom; Flags: disablenou
Name: "icons\desktopicon"; Description: "Create Desktop Shortcut"; Types: custom; Flags: disablenouninstallwarning
Name: "icons\quicklaunchicon"; Description: "Create Quick Launch Shortcut"; Types: custom; Flags: disablenouninstallwarning
Name: "translations"; Description: "Translations"; Types: normal custom; Flags: disablenouninstallwarning
Name: "spell"; Description: "Spelling Dictionaries"; Types: custom; Flags: disablenouninstallwarning
Name: "spell"; Description: "Spelling Dictionaries (Windows 7 fallback)"; Types: custom; Flags: disablenouninstallwarning
Name: "plugins"; Description: "Plugins"; Types: custom; Flags: disablenouninstallwarning
Name: "plugins\checksum"; Description: "Checksum"; Types: custom; Flags: disablenouninstallwarning
Name: "plugins\exec"; Description: "Exec"; Types: custom; Flags: disablenouninstallwarning
@@ -150,7 +150,7 @@ Source: "tiff.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "plugins\hcnotifications-winrt.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: libs
Source: "lib\enchant\*"; DestDir: "{app}\lib\enchant"; Flags: ignoreversion; Components: libs
Source: "lib\enchant-2\*"; DestDir: "{app}\lib\enchant-2"; Flags: ignoreversion; Components: libs
Source: "girepository-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua
Source: "lua51.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua
@@ -322,11 +322,12 @@ var
begin
GetWindowsVersionEx(Version);
// Windows 8 or greater has built in spell check.
if Version.NTPlatform and (Version.Major > 6) or ((Version.Major = 6) and (Version.Minor > 1)) then
// Enchant's WinSpell provider uses the built-in spell checker on Windows 8+.
// Downloaded dictionaries for other providers are retained only as a Windows 7 fallback.
if Version.NTPlatform and ((Version.Major > 6) or ((Version.Major = 6) and (Version.Minor > 1))) then
Result := True
else
Result := DirExists(ExpandConstant('{localappdata}') + '\enchant\myspell');
Result := DirExists(ExpandConstant('{localappdata}') + '\enchant-2\myspell');
end;
// Python >= 3.9 dropped support for Windows versions before 10;

View File

@@ -61,7 +61,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "installer", "installer\inst
ProjectSection(ProjectDependencies) = postProject
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {C2321A03-0BA7-45B3-8740-ABD82B36B0BF}
{D58924D2-1492-4AC3-B058-7A214424EEDC} = {D58924D2-1492-4AC3-B058-7A214424EEDC}
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}
{17E4BE39-76F7-4A06-AD21-EFD0C5091F76} = {17E4BE39-76F7-4A06-AD21-EFD0C5091F76}
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {4C0F3940-2EEE-4646-82F7-6CE75B9A72F4}
{B10A2C41-344C-43E0-A32D-B9587C198D8B} = {B10A2C41-344C-43E0-A32D-B9587C198D8B}
@@ -95,11 +94,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "notifications-winrt", "..\s
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "external", "external", "{021EC1D0-FF67-4700-9AB2-EAABF1159C09}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libenchant_win8", "..\src\libenchant_win8\libenchant_win8.vcxproj", "{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}"
ProjectSection(ProjectDependencies) = postProject
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "lua", "..\plugins\lua\lua.vcxproj", "{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4}"
EndProject
Global
@@ -137,8 +131,6 @@ Global
{D58924D2-1492-4AC3-B058-7A214424EEDC}.Release|x64.Build.0 = Release|x64
{C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.ActiveCfg = Release|x64
{C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.Build.0 = Release|x64
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}.Release|x64.ActiveCfg = Release|x64
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}.Release|x64.Build.0 = Release|x64
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4}.Release|x64.ActiveCfg = Release|x64
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4}.Release|x64.Build.0 = Release|x64
EndGlobalSection
@@ -161,7 +153,6 @@ Global
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
{D58924D2-1492-4AC3-B058-7A214424EEDC} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
{C53145CC-D021-40C9-B97C-0249AB9A43C9} = {561126F4-FA18-45FC-A2BF-8F858F161D6D}
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {021EC1D0-FF67-4700-9AB2-EAABF1159C09}
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
EndGlobalSection
EndGlobal