Skip to content

Commit a99db30

Browse files
committed
Use Windows system spell checker
1 parent ae51e96 commit a99db30

6 files changed

+252
-7
lines changed

core/CMakeLists.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ if(APPLE)
3737
list(APPEND SOURCES
3838
katvan_spellchecker_macos.mm
3939
)
40+
elseif(WIN32)
41+
list(APPEND SOURCES
42+
katvan_spellchecker_windows.cpp
43+
)
4044
else()
4145
list(APPEND SOURCES
4246
katvan_spellchecker_hunspell.cpp
@@ -59,7 +63,7 @@ target_link_libraries(katvan_core PUBLIC
5963
Qt6::Widgets
6064
)
6165

62-
if(NOT APPLE)
66+
if(NOT APPLE AND NOT WIN32)
6367
find_package(PkgConfig REQUIRED)
6468
pkg_check_modules(hunspell REQUIRED IMPORTED_TARGET hunspell)
6569

core/katvan_spellchecker.cpp

+6-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
*/
1818
#include "katvan_spellchecker.h"
1919

20-
#ifdef Q_OS_MACOS
20+
#if defined(Q_OS_MACOS)
2121
#include "katvan_spellchecker_macos.h"
22+
#elif defined(Q_OS_WINDOWS)
23+
#include "katvan_spellchecker_windows.h"
2224
#else
2325
#include "katvan_spellchecker_hunspell.h"
2426
#endif
@@ -44,8 +46,10 @@ SpellChecker* SpellChecker::instance()
4446
{
4547
static SpellChecker* checker = nullptr;
4648
if (!checker) {
47-
#ifdef Q_OS_MACOS
49+
#if defined(Q_OS_MACOS)
4850
checker = new MacOsSpellChecker();
51+
#elif defined(Q_OS_WINDOWS)
52+
checker = new WindowsSpellChecker();
4953
#else
5054
checker = new HunspellSpellChecker();
5155
#endif

core/katvan_spellchecker_windows.cpp

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* This file is part of Katvan
3+
* Copyright (c) 2024 - 2025 Igor Khanin
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
#include "katvan_spellchecker_windows.h"
19+
20+
#include <QDebug>
21+
22+
#include <objidl.h>
23+
#include <spellcheck.h>
24+
25+
namespace katvan {
26+
27+
WindowsSpellChecker::WindowsSpellChecker(QObject* parent)
28+
: SpellChecker(parent)
29+
, d_factory(nullptr)
30+
, d_checker(nullptr)
31+
{
32+
HRESULT hr = CoCreateInstance(__uuidof(SpellCheckerFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d_factory));
33+
if (FAILED(hr)) {
34+
qWarning() << "Failed to create SpellCheckerFactory:" << hr;
35+
}
36+
}
37+
38+
WindowsSpellChecker::~WindowsSpellChecker()
39+
{
40+
if (d_checker != nullptr) {
41+
d_checker->Release();
42+
}
43+
44+
if (d_factory != nullptr) {
45+
d_factory->Release();
46+
}
47+
}
48+
49+
QMap<QString, QString> WindowsSpellChecker::findDictionaries()
50+
{
51+
QMap<QString, QString> result;
52+
53+
if (d_factory == nullptr) {
54+
return result;
55+
}
56+
57+
IEnumString* languages = nullptr;
58+
HRESULT hr = d_factory->get_SupportedLanguages(&languages);
59+
if (FAILED(hr)) {
60+
qWarning() << "SpellCheckerFactory::get_SupportedLanguages failed:" << hr;
61+
return result;
62+
}
63+
64+
hr = S_OK;
65+
while (hr == S_OK) {
66+
LPOLESTR str = nullptr;
67+
hr = languages->Next(1, &str, nullptr);
68+
69+
if (hr == S_OK) {
70+
QString lang = QString::fromWCharArray(str);
71+
if (!lang.isEmpty()) {
72+
result.insert(lang, QString());
73+
}
74+
CoTaskMemFree(str);
75+
}
76+
}
77+
languages->Release();
78+
79+
return result;
80+
}
81+
82+
void WindowsSpellChecker::setCurrentDictionary(const QString& dictName, const QString& dictPath)
83+
{
84+
if (d_factory == nullptr) {
85+
return;
86+
}
87+
88+
if (d_checker != nullptr) {
89+
d_checker->Release();
90+
d_checker = nullptr;
91+
}
92+
93+
if (dictName.isEmpty()) {
94+
SpellChecker::setCurrentDictionary(dictName, dictPath);
95+
return;
96+
}
97+
std::wstring lang = dictName.toStdWString();
98+
99+
HRESULT hr = d_factory->CreateSpellChecker(lang.data(), &d_checker);
100+
if (FAILED(hr)) {
101+
qWarning() << "Failed to create spell checker for" << dictName << ":" << hr;
102+
return;
103+
}
104+
105+
SpellChecker::setCurrentDictionary(dictName, dictPath);
106+
}
107+
108+
SpellChecker::MisspelledWordRanges WindowsSpellChecker::checkSpelling(const QString& text)
109+
{
110+
SpellChecker::MisspelledWordRanges result;
111+
if (d_checker == nullptr) {
112+
return result;
113+
}
114+
115+
std::wstring str = text.toStdWString();
116+
117+
IEnumSpellingError* errors = nullptr;
118+
HRESULT hr = d_checker->ComprehensiveCheck(str.data(), &errors);
119+
if (FAILED(hr)) {
120+
qWarning() << "Failed comprehensive check:" << hr;
121+
return result;
122+
}
123+
124+
hr = S_OK;
125+
while (hr == S_OK) {
126+
ISpellingError* error = nullptr;
127+
128+
hr = errors->Next(&error);
129+
if (hr == S_OK) {
130+
ULONG start = 0, length = 0;
131+
error->get_StartIndex(&start);
132+
error->get_Length(&length);
133+
error->Release();
134+
135+
result.append(std::make_pair(start, length));
136+
}
137+
}
138+
errors->Release();
139+
140+
return result;
141+
}
142+
143+
void WindowsSpellChecker::addToPersonalDictionary(const QString& word)
144+
{
145+
if (d_checker == nullptr) {
146+
return;
147+
}
148+
149+
std::wstring str = word.toStdWString();
150+
151+
HRESULT hr = d_checker->Add(str.data());
152+
if (FAILED(hr)) {
153+
qWarning() << "SpellChecker::Add failed for" << word << ":" << hr;
154+
}
155+
}
156+
157+
void WindowsSpellChecker::requestSuggestionsImpl(const QString& word, int position)
158+
{
159+
if (d_checker == nullptr) {
160+
return;
161+
}
162+
163+
QStringList result;
164+
std::wstring str = word.toStdWString();
165+
166+
IEnumString* suggestions = nullptr;
167+
HRESULT hr = d_checker->Suggest(str.data(), &suggestions);
168+
if (FAILED(hr)) {
169+
qWarning() << "SpellChecker::Suggest failed for" << word << ":" << hr;
170+
return;
171+
}
172+
173+
hr = S_OK;
174+
while (hr == S_OK) {
175+
LPOLESTR suggestion = nullptr;
176+
hr = suggestions->Next(1, &suggestion, nullptr);
177+
178+
if (hr == S_OK) {
179+
result.append(QString::fromWCharArray(suggestion));
180+
CoTaskMemFree(suggestion);
181+
}
182+
}
183+
184+
suggestions->Release();
185+
suggestionsCalculated(word, position, result);
186+
}
187+
188+
}

core/katvan_spellchecker_windows.h

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* This file is part of Katvan
3+
* Copyright (c) 2024 - 2025 Igor Khanin
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
#pragma once
19+
20+
#include "katvan_spellchecker.h"
21+
22+
struct ISpellCheckerFactory;
23+
struct ISpellChecker;
24+
25+
namespace katvan {
26+
27+
class WindowsSpellChecker : public SpellChecker
28+
{
29+
Q_OBJECT
30+
31+
public:
32+
WindowsSpellChecker(QObject* parent = nullptr);
33+
~WindowsSpellChecker();
34+
35+
QMap<QString, QString> findDictionaries() override;
36+
37+
void setCurrentDictionary(const QString& dictName, const QString& dictPath) override;
38+
39+
MisspelledWordRanges checkSpelling(const QString& text) override;
40+
41+
void addToPersonalDictionary(const QString& word) override;
42+
43+
private:
44+
void requestSuggestionsImpl(const QString& word, int position) override;
45+
46+
ISpellCheckerFactory* d_factory;
47+
ISpellChecker* d_checker;
48+
};
49+
50+
}

tests/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ add_executable(katvan_tests
1414
main.cpp
1515
)
1616

17-
if(NOT APPLE)
17+
if(NOT APPLE AND NOT WIN32)
1818
target_sources(katvan_tests PRIVATE katvan_spellchecker.t.cpp)
1919
endif()
2020

vcpkg.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
33
"builtin-baseline": "53bef8994c541b6561884a8395ea35715ece75db",
44
"dependencies": [
5-
"hunspell",
65
"gtest",
76
{
87
"name": "libarchive",
98
"default-features": false
109
},
1110
{
12-
"name": "pkgconf",
13-
"platform": "windows"
11+
"name": "hunspell",
12+
"platform": "!windows & !osx"
1413
}
1514
]
1615
}

0 commit comments

Comments
 (0)