* feat(engine): 支持 helpcodes/custom 下的自定义辅助码 custom 目录里的每个 .txt 都是一套方案,方案名为 custom/<文件名>。 文件开头的 "# name:" / "# name_en:" 注释给出中英文显示名,缺省用文件名; 兼容 BOM、CRLF 和全角冒号。方案名不能跳出 custom 目录。 * feat(engine): WebView 契约新增 openHelpcodeDirectory 设置页请求打开自定义辅助码目录,仅允许 settings 页发送。同步 ui-html/webview2/shared。 * feat(server): 设置页列出自定义辅助码并可打开其目录 配置快照带上 helpcode.custom_schemas 与 helpcode.custom_directory,每次都会重新扫描; 方案文件被删除或改名后回退到 lantian。两个设置宿主都会处理 openHelpcodeDirectory。 * feat(ui-html): 辅助码设置显示拼音加加与自定义方案 内置方案“加加”改名为“拼音加加”。自定义方案追加在内置方案之后;新增说明区, 提供“打开目录”和“刷新方案”两个按钮,刷新完成后弹出提示。 * feat(installer): 升级与迁移数据目录时保留自定义辅助码 升级清理时只清掉 helpcodes 里的内置表,保留 custom 子目录;更换数据目录时一并迁移; 安装时建出 helpcodes\custom。
1941 lines
84 KiB
C++
1941 lines
84 KiB
C++
#include "engine/contracts/webview/validator.h"
|
||
#include "config/ime_config.h"
|
||
#include "engine/core/data_path.h"
|
||
#include "global/globals.h"
|
||
#include "resource/resource.h"
|
||
#include "settings/settings_launcher.h"
|
||
#include "settings/api_credential_test.h"
|
||
#include "settings/settings_splash.h"
|
||
#include "settings/dictionary_manager.h"
|
||
#include "settings/serial_task_queue.h"
|
||
#include "statistics/stats_overview.h"
|
||
#include "statistics/stats_store.h"
|
||
#include <memory>
|
||
#include <stdexcept>
|
||
#include "skin/candidate_skin_catalog.h"
|
||
#include "utils/common_utils.h"
|
||
#include "utils/single_instance.h"
|
||
#include "voice-input/voice_providers.h"
|
||
|
||
#include <WebView2.h>
|
||
#include <WebView2EnvironmentOptions.h>
|
||
#include <WebView2EnvironmentOptions.h>
|
||
#include <boost/json.hpp>
|
||
#include <dcomp.h>
|
||
#include <dwmapi.h>
|
||
#include <nlohmann/json.hpp>
|
||
#include <wil/com.h>
|
||
#include <windows.h>
|
||
#include <windowsx.h>
|
||
#include <shellapi.h>
|
||
#include <wrl.h>
|
||
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <filesystem>
|
||
#include <optional>
|
||
#include <string>
|
||
#include <string_view>
|
||
|
||
#pragma comment(lib, "dcomp.lib")
|
||
#pragma comment(lib, "dwmapi.lib")
|
||
#pragma comment(lib, "shell32.lib")
|
||
|
||
using Microsoft::WRL::Callback;
|
||
using Microsoft::WRL::ComPtr;
|
||
using Microsoft::WRL::Make;
|
||
namespace json = boost::json;
|
||
|
||
namespace
|
||
{
|
||
constexpr wchar_t kWindowClass[] = L"MetasequoiaImeSettingsWindow";
|
||
constexpr wchar_t kWindowTitle[] = L"Metasequoia IME Settings";
|
||
constexpr wchar_t kSingleInstanceMutex[] = L"Local\\MetasequoiaImeSettings.SingleInstance";
|
||
constexpr UINT kActivateExistingWindow = WM_APP + 1;
|
||
constexpr UINT kOpenAboutSection = WM_APP + 2;
|
||
constexpr UINT kScanSkinCatalog = WM_APP + 3;
|
||
constexpr UINT kWorkerCompleted = WM_APP + 4;
|
||
constexpr UINT kQuitSettings = WM_APP + 5;
|
||
constexpr UINT_PTR kConfigReloadTimer = 1;
|
||
constexpr UINT_PTR kLingerTimer = 2;
|
||
// 关闭窗口先只隐藏,进程留着已导航完成的 WebView2;这段时间内重新打开就不用再冷启动一次。
|
||
constexpr UINT kLingerTimeoutMs = 10 * 60 * 1000;
|
||
|
||
ComPtr<ICoreWebView2Controller> g_controller;
|
||
ComPtr<ICoreWebView2CompositionController> g_composition_controller;
|
||
ComPtr<ICoreWebView2> g_webview;
|
||
ComPtr<ICoreWebView2_3> g_webview3;
|
||
ComPtr<ICoreWebView2Controller2> g_controller2;
|
||
ComPtr<IDCompositionDevice> g_dcomp_device;
|
||
ComPtr<IDCompositionTarget> g_dcomp_target;
|
||
ComPtr<IDCompositionVisual> g_dcomp_root;
|
||
RECT g_maximize_button_rect{};
|
||
bool g_has_maximize_button_rect = false;
|
||
bool g_maximize_button_hover = false;
|
||
bool g_window_active = true;
|
||
bool g_window_minimized = false;
|
||
bool g_open_about_on_ready = false;
|
||
bool g_webview_content_started = false;
|
||
HWND g_settings_hwnd = nullptr;
|
||
std::optional<CandidateSkinCatalog::ScanResult> g_candidate_skin_catalog;
|
||
uint64_t g_candidate_skin_catalog_revision = 0;
|
||
std::unique_ptr<SerialTaskQueue> g_worker;
|
||
bool g_closing = false;
|
||
bool g_lingering = false;
|
||
bool g_reload_pending = false;
|
||
bool g_settings_light = false;
|
||
std::wstring g_last_config_message;
|
||
bool g_worker_com_initialized = false; // worker-only
|
||
void CloseSettings(HWND hwnd);
|
||
void HideSettings(HWND hwnd);
|
||
|
||
nlohmann::json CandidateColorsToJson(const CandidateSkinCatalog::CandidateColors &colors)
|
||
{
|
||
nlohmann::json json = nlohmann::json::object();
|
||
if (!colors.accent.empty())
|
||
json["accent"] = colors.accent;
|
||
if (!colors.selected.empty())
|
||
json["selected"] = colors.selected;
|
||
if (!colors.hover.empty())
|
||
json["hover"] = colors.hover;
|
||
if (!colors.surface.empty())
|
||
json["surface"] = colors.surface;
|
||
if (!colors.border.empty())
|
||
json["border"] = colors.border;
|
||
if (!colors.text.empty())
|
||
json["text"] = colors.text;
|
||
if (!colors.number.empty())
|
||
json["number"] = colors.number;
|
||
if (colors.showSelectedBar.has_value())
|
||
json["showSelectedBar"] = *colors.showSelectedBar;
|
||
return json;
|
||
}
|
||
|
||
void ShowAboutSection()
|
||
{
|
||
if (!g_webview)
|
||
return;
|
||
g_webview->ExecuteScript(LR"JS((() => {
|
||
const selectAbout = () => {
|
||
const item = document.querySelector('.sidebar .item[data-target="about-settings"]');
|
||
if (!item) return false;
|
||
if (!item.classList.contains('active')) item.click();
|
||
const section = document.getElementById('about-settings');
|
||
if (!item.classList.contains('active') || !section || getComputedStyle(section).display === 'none')
|
||
return false;
|
||
item.scrollIntoView({ block: 'nearest' });
|
||
return true;
|
||
};
|
||
if (selectAbout()) return;
|
||
const retry = setInterval(() => {
|
||
if (selectAbout()) clearInterval(retry);
|
||
}, 50);
|
||
setTimeout(() => clearInterval(retry), 10000);
|
||
})())JS",
|
||
nullptr);
|
||
}
|
||
|
||
void ApplyWindowActivationAppearance()
|
||
{
|
||
if (!g_webview)
|
||
return;
|
||
|
||
// Keep this effect in the native host so it follows the real top-level
|
||
// activation state rather than DOM focus. Only title-bar foreground
|
||
// elements are muted; the page and title-bar backgrounds remain intact.
|
||
const wchar_t *script = g_window_active ? LR"JS((() => {
|
||
document.getElementById('metasequoia-window-inactive-overlay')?.remove();
|
||
const id = 'metasequoia-window-activation-style';
|
||
if (!document.getElementById(id)) {
|
||
const style = document.createElement('style');
|
||
style.id = id;
|
||
style.textContent = `
|
||
.titlebar-left, .window-button {
|
||
transition: opacity 140ms ease-out;
|
||
}
|
||
html.metasequoia-window-inactive .titlebar-left,
|
||
html.metasequoia-window-inactive .window-button {
|
||
opacity: 0.52 !important;
|
||
}
|
||
html.metasequoia-window-inactive .window-button:hover,
|
||
html.metasequoia-window-inactive .window-button.host-hover {
|
||
opacity: 1 !important;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
document.documentElement.classList.remove('metasequoia-window-inactive');
|
||
})())JS"
|
||
: LR"JS((() => {
|
||
document.getElementById('metasequoia-window-inactive-overlay')?.remove();
|
||
const id = 'metasequoia-window-activation-style';
|
||
if (!document.getElementById(id)) {
|
||
const style = document.createElement('style');
|
||
style.id = id;
|
||
style.textContent = `
|
||
.titlebar-left, .window-button {
|
||
transition: opacity 140ms ease-out;
|
||
}
|
||
html.metasequoia-window-inactive .titlebar-left,
|
||
html.metasequoia-window-inactive .window-button {
|
||
opacity: 0.52 !important;
|
||
}
|
||
html.metasequoia-window-inactive .window-button:hover,
|
||
html.metasequoia-window-inactive .window-button.host-hover {
|
||
opacity: 1 !important;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
document.documentElement.classList.add('metasequoia-window-inactive');
|
||
})())JS";
|
||
g_webview->ExecuteScript(script, nullptr);
|
||
}
|
||
|
||
void ApplySettingsChromeTheme(HWND hwnd = nullptr)
|
||
{
|
||
const HWND target = hwnd ? hwnd : g_settings_hwnd;
|
||
const bool light = g_settings_light;
|
||
if (target)
|
||
{
|
||
BOOL dark = light ? FALSE : TRUE;
|
||
DwmSetWindowAttribute(target, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark));
|
||
}
|
||
if (g_controller2)
|
||
{
|
||
COREWEBVIEW2_COLOR background =
|
||
light ? COREWEBVIEW2_COLOR{255, 243, 243, 243} : COREWEBVIEW2_COLOR{255, 32, 32, 32};
|
||
g_controller2->put_DefaultBackgroundColor(background);
|
||
}
|
||
}
|
||
|
||
void ResetTitlebarHoverAfterVisibilityChange()
|
||
{
|
||
if (!g_webview)
|
||
return;
|
||
|
||
g_webview->ExecuteScript(LR"JS((() => {
|
||
const root = document.documentElement;
|
||
const suppressClass = 'metasequoia-suppress-titlebar-tooltip';
|
||
const styleId = 'metasequoia-titlebar-tooltip-reset-style';
|
||
if (!document.getElementById(styleId)) {
|
||
const style = document.createElement('style');
|
||
style.id = styleId;
|
||
style.textContent = `
|
||
html.${suppressClass} .window-button[data-tooltip]::after,
|
||
html.${suppressClass} .window-button[data-tooltip]:hover::after {
|
||
opacity: 0 !important;
|
||
transform: translateX(-50%) !important;
|
||
transition-delay: 0s !important;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
root.classList.add(suppressClass);
|
||
document.querySelector('.window-controls')
|
||
?.classList.add('window-controls-click-reset');
|
||
document.querySelectorAll('.window-button').forEach((button) => {
|
||
button.classList.remove('host-hover', 'host-active');
|
||
if (button instanceof HTMLElement) button.blur();
|
||
});
|
||
|
||
const previousRelease = window.__metasequoiaReleaseTooltipReset;
|
||
if (typeof previousRelease === 'function') {
|
||
window.removeEventListener('pointermove', previousRelease, true);
|
||
window.removeEventListener('pointerdown', previousRelease, true);
|
||
}
|
||
const release = () => {
|
||
root.classList.remove(suppressClass);
|
||
window.removeEventListener('pointermove', release, true);
|
||
window.removeEventListener('pointerdown', release, true);
|
||
delete window.__metasequoiaReleaseTooltipReset;
|
||
};
|
||
window.__metasequoiaReleaseTooltipReset = release;
|
||
window.addEventListener('pointermove', release, {capture: true, once: true});
|
||
window.addEventListener('pointerdown', release, {capture: true, once: true});
|
||
})())JS",
|
||
nullptr);
|
||
}
|
||
|
||
static nlohmann::json CustomHelpcodeSchemasJson()
|
||
{
|
||
nlohmann::json schemas = nlohmann::json::array();
|
||
for (const auto &schema : GetCustomHelpcodeSchemas())
|
||
schemas.push_back({{"id", schema.schema}, {"name", schema.name}, {"name_en", schema.name_en}});
|
||
return schemas;
|
||
}
|
||
|
||
std::wstring BuildConfigMessage(bool refresh_skin_catalog)
|
||
{
|
||
|
||
const VoiceInputConfig &voice = GetConfiguredVoiceInput();
|
||
const AiAssistantConfig &ai = GetConfiguredAiAssistant();
|
||
const TencentTmtConfig &tencent_tmt = GetConfiguredTencentTmt();
|
||
const CustomTranslationConfig &custom_translation = GetConfiguredCustomTranslation();
|
||
const NiuTransConfig &niutrans = GetConfiguredNiuTrans();
|
||
const FrequencyAdjustmentConfig &frequency = GetConfiguredFrequencyAdjustment();
|
||
const FloatingToolbarItemsConfig &toolbar = GetConfiguredFloatingToolbarItems();
|
||
const std::filesystem::path skins_root = std::filesystem::path(CommonUtils::get_ime_data_path_w()) / L"skins";
|
||
if (refresh_skin_catalog)
|
||
{
|
||
g_candidate_skin_catalog = CandidateSkinCatalog::Scan(skins_root);
|
||
++g_candidate_skin_catalog_revision;
|
||
}
|
||
const std::string skin_layout = GetConfiguredCandidateWindowLayout();
|
||
const std::string skin_theme = ResolveConfiguredTheme(GetConfiguredThemeCand());
|
||
nlohmann::json external_skins = nlohmann::json::array();
|
||
if (g_candidate_skin_catalog)
|
||
{
|
||
for (const auto &skin : g_candidate_skin_catalog->packages)
|
||
{
|
||
external_skins.push_back(
|
||
{{"id", skin.id},
|
||
{"name", skin.name},
|
||
{"version", skin.version},
|
||
{"author", skin.author},
|
||
{"description", skin.description},
|
||
{"base", skin.base},
|
||
{"toolbarStylesheet", skin.toolbarStylesheet},
|
||
{"preview", skin.preview},
|
||
{"layouts", skin.layouts},
|
||
{"themes", skin.themes},
|
||
{"minWidthDip", skin.minWidthDip},
|
||
{"decorationTopDip", skin.decorationTopDip},
|
||
{"decorationWidthDip", skin.decorationWidthDip},
|
||
{"candidate",
|
||
{{"dark", CandidateColorsToJson(skin.dark)}, {"light", CandidateColorsToJson(skin.light)}}},
|
||
{"compatible", CandidateSkinCatalog::Supports(skin, skin_layout, skin_theme)}});
|
||
}
|
||
}
|
||
nlohmann::json skin_issues = nlohmann::json::array();
|
||
if (g_candidate_skin_catalog)
|
||
{
|
||
for (const auto &issue : g_candidate_skin_catalog->issues)
|
||
skin_issues.push_back({{"folder", issue.folder}, {"reason", issue.reason}});
|
||
}
|
||
nlohmann::json polish_presets = nlohmann::json::array();
|
||
for (const auto &preset : VoiceInput::BuiltinPolishPromptPresets())
|
||
{
|
||
polish_presets.push_back({{"id", std::string(preset.id)},
|
||
{"name", std::string(preset.name)},
|
||
{"prompt", std::string(preset.prompt)}});
|
||
}
|
||
// 规则位取不门控视图:总开关关闭时复选框仍要展示用户已存的勾选;
|
||
// 总开关本身单独下发。
|
||
const metasequoia::FuzzyPinyinOptions fuzzy_rules = GetConfiguredFuzzyPinyinRuleStates();
|
||
nlohmann::json payload = {
|
||
{"type", "configSnapshot"},
|
||
{"data",
|
||
{{"input",
|
||
{{"mode", GetConfiguredInputMode()},
|
||
{"schema", GetConfiguredInputSchemeName()},
|
||
{"japanese_schema", GetConfiguredJapaneseSchema()},
|
||
{"character_set", GetConfiguredCharacterSet()},
|
||
{"default_ime_mode", GetConfiguredDefaultImeMode()},
|
||
{"ime_mode_scope", GetConfiguredImeModeScope()},
|
||
{"shuangpin_schema", GetConfiguredShuangpinSchema()},
|
||
{"wubi_schema", GetConfiguredWubiSchema()},
|
||
{"word_to_character", GetConfiguredWordToCharacterEnabled()},
|
||
{"word_to_character_keys", GetConfiguredWordToCharacterKeys()},
|
||
{"smart_punctuation", GetConfiguredSmartPunctuationEnabled()},
|
||
{"smart_punctuation_space_convert", GetConfiguredSmartPunctuationSpaceConvertEnabled()},
|
||
{"smart_punctuation_direct_digit", GetConfiguredSmartPunctuationDirectDigitEnabled()},
|
||
{"smart_punctuation_direct_letter", GetConfiguredSmartPunctuationDirectLetterEnabled()},
|
||
{"smart_punctuation_repeat_to_chinese", GetConfiguredSmartPunctuationRepeatToChineseEnabled()},
|
||
{"paired_punctuation", GetConfiguredPairedPunctuationEnabled()},
|
||
{"punctuation_lock", GetConfiguredPunctuationLock()},
|
||
{"fuzzy_pinyin", GetConfiguredFuzzyPinyinEnabled()},
|
||
{"fuzzy_z_zh", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::Z_ZH)},
|
||
{"fuzzy_c_ch", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::C_CH)},
|
||
{"fuzzy_s_sh", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::S_SH)},
|
||
{"fuzzy_n_l", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::N_L)},
|
||
{"fuzzy_f_h", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::F_H)},
|
||
{"fuzzy_r_l", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::R_L)},
|
||
{"fuzzy_an_ang", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::AN_ANG)},
|
||
{"fuzzy_en_eng", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::EN_ENG)},
|
||
{"fuzzy_in_ing", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::IN_ING)},
|
||
{"fuzzy_ian_iang", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::IAN_IANG)},
|
||
{"fuzzy_uan_uang", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::UAN_UANG)}}},
|
||
{"general",
|
||
{{"diagnostic_log", GetConfiguredDiagnosticLogEnabled()},
|
||
{"candidate_window_diagnostic_log", GetConfiguredDiagnosticLogEnabled()},
|
||
{"tsf_diagnostic_log", GetConfiguredTsfDiagnosticLogEnabled()},
|
||
{"floating_toolbar", GetConfiguredFloatingToolbarEnabled()},
|
||
{"caret_state_indicator", GetConfiguredCaretStateIndicatorEnabled()},
|
||
{"caret_state_indicator_on_focus", GetConfiguredCaretStateIndicatorOnFocus()},
|
||
{"caret_state_indicator_position", GetConfiguredCaretStateIndicatorPosition()},
|
||
{"floating_toolbar_fullwidth", toolbar.fullwidth},
|
||
{"floating_toolbar_punctuation", toolbar.punctuation},
|
||
{"floating_toolbar_character_set", toolbar.character_set},
|
||
{"floating_toolbar_emoji", toolbar.emoji},
|
||
{"floating_toolbar_screen_keyboard", toolbar.screen_keyboard},
|
||
{"floating_toolbar_settings", toolbar.settings},
|
||
{"floating_toolbar_scale", GetConfiguredFloatingToolbarScale()},
|
||
{"floating_toolbar_font_size", GetConfiguredFloatingToolbarFontSize()},
|
||
{"cn_en_mixed_input", GetConfiguredEnglishCandidatesEnabled()},
|
||
{"candidate_translations", GetConfiguredCandidateTranslationsEnabled()},
|
||
{"cn_en_mixed_input_min_chars", GetConfiguredEnglishMixedInputMinChars()},
|
||
{"emoji_mixed_input", GetConfiguredEmojiMixedInputEnabled()},
|
||
{"kaomoji_mixed_input", GetConfiguredKaomojiMixedInputEnabled()},
|
||
{"cloud_candidates", GetConfiguredCloudCandidatesEnabled()},
|
||
{"paging_minus_equal", GetConfiguredPagingMinusEqualEnabled()},
|
||
{"paging_comma_period", GetConfiguredPagingCommaPeriodEnabled()},
|
||
{"paging_brackets", GetConfiguredPagingBracketsEnabled()},
|
||
{"paging_tab", GetConfiguredPagingTabEnabled()},
|
||
{"paging_page_up_down", GetConfiguredPagingPageUpDownEnabled()},
|
||
{"paging_mouse_wheel", GetConfiguredPagingMouseWheelEnabled()},
|
||
{"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()}}},
|
||
{"association",
|
||
{{"sentence_wordlattice", GetConfiguredAssocSentenceWordLattice()},
|
||
{"sentence_google", GetConfiguredAssocSentenceGoogle()},
|
||
{"sentence_neural_desktop", GetConfiguredAssocSentenceNeuralDesktop()},
|
||
{"sentence_neural_keyboard", GetConfiguredAssocSentenceNeuralKeyboard()},
|
||
{"sentence_show_next_on_duplicate", GetConfiguredAssocSentenceShowNextOnDuplicate()}}},
|
||
{"keybindings",
|
||
{{"switch_language_shift", GetConfiguredSwitchLanguageShiftEnabled()},
|
||
{"switch_language_ctrl", GetConfiguredSwitchLanguageCtrlEnabled()},
|
||
{"switch_language_ctrl_alt_space", GetConfiguredSwitchLanguageCtrlAltSpaceEnabled()},
|
||
{"toggle_character_set_ctrl_shift_f", GetConfiguredCharacterSetShortcutEnabled()}}},
|
||
{"frequency_adjustment",
|
||
{{"mode", frequency.mode},
|
||
{"trigger_count", frequency.trigger_count},
|
||
{"linear_step", frequency.linear_step}}},
|
||
{"utility",
|
||
{{"unicode_mode", GetConfiguredUnicodeModeEnabled()},
|
||
{"quick_phrase", GetConfiguredQuickPhraseEnabled()},
|
||
{"date_time_mode", GetConfiguredDateTimeModeEnabled()},
|
||
{"emoji_mode", GetConfiguredEmojiModeEnabled()},
|
||
{"kaomoji_mode", GetConfiguredKaomojiModeEnabled()},
|
||
{"jianpin_mode", GetConfiguredJianpinModeEnabled()},
|
||
{"y_mode", GetConfiguredYModeEnabled()},
|
||
{"r_mode", GetConfiguredRModeEnabled()},
|
||
{"clipboard_history", GetConfiguredClipboardHistoryEnabled()}}},
|
||
{"appearance",
|
||
{{"ui_backend", GetConfiguredUiBackend()},
|
||
{"candidate_window_layout", GetConfiguredCandidateWindowLayout()},
|
||
{"candidate_window_follow_cursor", GetConfiguredCandidateWindowFollowCursor()},
|
||
{"candidate_skin", GetConfiguredCandidateSkin()},
|
||
{"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()},
|
||
{"candidate_fixed_badge", GetConfiguredCandidateFixedBadge()},
|
||
{"candidate_fixed_badge_style", GetConfiguredCandidateFixedBadgeStyle()},
|
||
{"tsf_preedit_style", GetConfiguredTsfPreeditStyle()},
|
||
{"theme_mode", GetConfiguredThemeMode()},
|
||
{"theme_settings", GetConfiguredThemeSettings()},
|
||
{"theme_cand", GetConfiguredThemeCand()},
|
||
{"theme_ftb", GetConfiguredThemeFtb()},
|
||
{"theme_menu", GetConfiguredThemeMenu()},
|
||
{"theme_emoji", GetConfiguredThemeEmoji()},
|
||
{"theme_screen_keyboard", GetConfiguredThemeScreenKeyboard()},
|
||
{"theme_handwriting", GetConfiguredThemeHandwriting()},
|
||
{"theme_voice", GetConfiguredThemeVoice()},
|
||
{"page_size", GetConfiguredCandidatePageSize()},
|
||
{"font", GetConfiguredCandidateFont()},
|
||
{"font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateFont())},
|
||
{"fallback_fonts", GetConfiguredCandidateFallbackFonts()},
|
||
{"fallback_font_css_families", GetConfiguredCandidateFallbackFontFamilies()},
|
||
{"english_font", GetConfiguredCandidateEnglishFont()},
|
||
{"english_font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateEnglishFont())},
|
||
{"default_font", GetConfiguredCandidateDefaultFont()},
|
||
{"default_font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateDefaultFont())},
|
||
{"font_size", GetConfiguredCandidateFontSize()},
|
||
{"candidate_window_preedit_font_size", GetConfiguredCandidateWindowPreeditFontSize()},
|
||
{"cand_text_color", GetConfiguredCandidateTextColor()},
|
||
{"system_fonts", GetSystemFontFamilies()},
|
||
{"external_candidate_skins", std::move(external_skins)},
|
||
{"candidate_skin_scan_issues", std::move(skin_issues)},
|
||
{"candidate_skin_catalog_scanned", g_candidate_skin_catalog.has_value()},
|
||
{"candidate_skin_catalog_revision", g_candidate_skin_catalog_revision},
|
||
{"candidate_skin_directory", skins_root.u8string()}}},
|
||
{"voice_input",
|
||
{{"enabled", voice.enabled},
|
||
{"hotkey_ralt", voice.hotkey_ralt},
|
||
{"hotkey_ctrl_f9", voice.hotkey_ctrl_f9},
|
||
{"hotkey_ctrl_win", voice.hotkey_ctrl_win},
|
||
{"hotkey_rctrl_ralt", voice.hotkey_rctrl_ralt},
|
||
{"hotkey_hold_space_lock", voice.hotkey_hold_space_lock},
|
||
{"asr_provider", voice.asr_provider},
|
||
{"doubao_auth_mode", voice.doubao_auth_mode},
|
||
{"asr_app_key", voice.asr_app_key},
|
||
{"asr_token", voice.asr_token},
|
||
{"asr_tokens", voice.asr_tokens},
|
||
{"asr_endpoint", voice.asr_endpoint},
|
||
{"asr_resource_id", voice.asr_resource_id},
|
||
{"doubao_enable_itn", voice.doubao_enable_itn},
|
||
{"doubao_enable_punc", voice.doubao_enable_punc},
|
||
{"doubao_enable_ddc", voice.doubao_enable_ddc},
|
||
{"doubao_boosting_table_id", voice.doubao_boosting_table_id},
|
||
{"asr_model", voice.asr_model},
|
||
{"polish_provider", voice.polish_provider},
|
||
{"polish_token", voice.polish_token},
|
||
{"polish_tokens", voice.polish_tokens},
|
||
{"polish_endpoint", voice.polish_endpoint},
|
||
{"polish_model", voice.polish_model},
|
||
{"polish_prompt_id", voice.polish_prompt_id},
|
||
{"polish_prompt", voice.polish_prompt},
|
||
{"polish_prompt_custom_1", voice.polish_prompt_custom_1},
|
||
{"polish_prompt_custom_2", voice.polish_prompt_custom_2},
|
||
{"polish_prompt_custom_3", voice.polish_prompt_custom_3},
|
||
{"language", voice.language},
|
||
{"start_sound", voice.start_sound},
|
||
{"end_sound", voice.end_sound},
|
||
{"mute_system_audio", voice.mute_system_audio},
|
||
{"notification_sound", voice.start_sound && voice.end_sound},
|
||
{"polish_text", voice.polish_text},
|
||
{"stream_inline_preedit", voice.stream_inline_preedit},
|
||
{"commit_mode", voice.commit_mode}}},
|
||
{"ai_assistant",
|
||
{{"enabled", ai.enabled},
|
||
{"provider", ai.provider},
|
||
{"token", ai.token},
|
||
{"tokens", ai.tokens},
|
||
{"endpoint", ai.endpoint},
|
||
{"model", ai.model},
|
||
{"candidate_limit", ai.candidate_limit},
|
||
{"prompt", ai.prompt},
|
||
{"prompt_id", ai.prompt_id},
|
||
{"prompt_custom_1", ai.prompt_custom_1},
|
||
{"prompt_custom_2", ai.prompt_custom_2},
|
||
{"prompt_custom_3", ai.prompt_custom_3}}},
|
||
{"tencent_tmt",
|
||
{{"secret_id", tencent_tmt.secret_id},
|
||
{"secret_key", tencent_tmt.secret_key},
|
||
{"region", tencent_tmt.region},
|
||
{"target_language", tencent_tmt.target_language}}},
|
||
{"custom_translation",
|
||
{{"enabled", custom_translation.enabled},
|
||
{"endpoint", custom_translation.endpoint},
|
||
{"api_key", custom_translation.api_key}}},
|
||
{"niutrans", {{"enabled", niutrans.enabled}, {"app_id", niutrans.app_id}, {"apikey", niutrans.apikey}}},
|
||
{"helpcode",
|
||
{{"shuangpin_helpcode", GetConfiguredShuangpinHelpcodeEnabled()},
|
||
{"shuangpin_helpcode_schema", GetConfiguredShuangpinHelpcodeSchema()},
|
||
{"quanpin_helpcode", GetConfiguredQuanpinHelpcodeEnabled()},
|
||
{"quanpin_helpcode_schema", GetConfiguredQuanpinHelpcodeSchema()},
|
||
{"show_sp_helpcode_in_candidate_window", GetConfiguredShowShuangpinHelpcodeInCandidateWindow()},
|
||
{"show_qp_helpcode_in_candidate_window", GetConfiguredShowQuanpinHelpcodeInCandidateWindow()}}},
|
||
{"quanpin",
|
||
{{"autocorrect_transposition", GetConfiguredQuanpinAutocorrectTransposition()},
|
||
{"autocorrect_neighbor", GetConfiguredQuanpinAutocorrectNeighbor()}}},
|
||
{"statistics",
|
||
{{"enabled", GetConfiguredStatisticsEnabled()}, {"retention", GetConfiguredStatisticsRetention()}}}}}};
|
||
payload["data"]["voice_input"]["polish_presets"] = std::move(polish_presets);
|
||
payload["data"]["helpcode"]["custom_schemas"] = CustomHelpcodeSchemasJson();
|
||
payload["data"]["helpcode"]["custom_directory"] = GetCustomHelpcodeDirectory();
|
||
payload["protocolVersion"] = metasequoia::webview::Version;
|
||
const std::string serialized = payload.dump();
|
||
if (!metasequoia::webview::Validate(json::parse(serialized), "server"))
|
||
throw std::runtime_error("Invalid config snapshot");
|
||
return string_to_wstring(serialized);
|
||
}
|
||
|
||
SerialTaskQueue::Completion ConfigCompletion(bool refresh_skin_catalog = false)
|
||
{
|
||
auto message = BuildConfigMessage(refresh_skin_catalog);
|
||
const bool light = ResolveConfiguredTheme(GetConfiguredThemeSettings()) == "light";
|
||
return [message = std::move(message), light] {
|
||
g_settings_light = light;
|
||
ApplySettingsChromeTheme();
|
||
SettingsSplash::SetTheme(light);
|
||
if (g_webview && message != g_last_config_message)
|
||
{
|
||
if (SUCCEEDED(g_webview->PostWebMessageAsJson(message.c_str())))
|
||
g_last_config_message = message;
|
||
}
|
||
};
|
||
}
|
||
|
||
void PostConfig(bool refresh_skin_catalog = false)
|
||
{
|
||
if (!g_worker || g_closing)
|
||
return;
|
||
g_worker->Submit([refresh_skin_catalog] { return ConfigCompletion(refresh_skin_catalog); });
|
||
}
|
||
|
||
void PostValidatedServerMessage(json::object payload)
|
||
{
|
||
if (!g_webview)
|
||
return;
|
||
payload["protocolVersion"] = metasequoia::webview::Version;
|
||
if (!metasequoia::webview::Validate(payload, "server"))
|
||
return;
|
||
const std::wstring message = string_to_wstring(json::serialize(payload));
|
||
g_webview->PostWebMessageAsJson(message.c_str());
|
||
}
|
||
|
||
void PostWindowState(HWND hwnd)
|
||
{
|
||
PostValidatedServerMessage({
|
||
{"type", "windowState"},
|
||
{"data", {{"isMaximized", IsZoomed(hwnd) != FALSE}}},
|
||
});
|
||
}
|
||
|
||
void PostMaximizeButtonEvent(const char *event_name)
|
||
{
|
||
PostValidatedServerMessage({
|
||
{"type", "maxButtonEvent"},
|
||
{"data", {{"event", event_name}}},
|
||
});
|
||
}
|
||
|
||
bool ApplyConfigUpdate(const json::object &data)
|
||
{
|
||
const std::string path = json::value_to<std::string>(data.at("path"));
|
||
if (path == "input.mode")
|
||
return SetConfiguredInputMode(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.schema")
|
||
return SetConfiguredInputScheme(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.japanese_schema")
|
||
return SetConfiguredJapaneseSchema(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.character_set")
|
||
return SetConfiguredCharacterSet(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.default_ime_mode")
|
||
return SetConfiguredDefaultImeMode(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.ime_mode_scope")
|
||
return SetConfiguredImeModeScope(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.shuangpin_schema")
|
||
return SetConfiguredShuangpinSchema(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.wubi_schema")
|
||
return SetConfiguredWubiSchema(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.word_to_character")
|
||
return SetConfiguredWordToCharacterEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.word_to_character_keys")
|
||
return SetConfiguredWordToCharacterKeys(json::value_to<std::string>(data.at("value")));
|
||
if (path == "input.smart_punctuation")
|
||
return SetConfiguredSmartPunctuationEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.smart_punctuation_space_convert")
|
||
return SetConfiguredSmartPunctuationSpaceConvertEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.smart_punctuation_direct_digit")
|
||
return SetConfiguredSmartPunctuationDirectDigitEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.smart_punctuation_direct_letter")
|
||
return SetConfiguredSmartPunctuationDirectLetterEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.smart_punctuation_repeat_to_chinese")
|
||
return SetConfiguredSmartPunctuationRepeatToChineseEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.paired_punctuation")
|
||
return SetConfiguredPairedPunctuationEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "input.punctuation_lock")
|
||
return SetConfiguredPunctuationLock(json::value_to<std::string>(data.at("value")));
|
||
constexpr std::string_view input_section_prefix = "input.";
|
||
constexpr std::string_view fuzzy_prefix = "input.fuzzy_";
|
||
// 精确分支必须在前:总开关键同样命中 fuzzy_ 前缀,不能靠 prefix setter 拒绝它。
|
||
if (path == "input.fuzzy_pinyin")
|
||
return SetConfiguredFuzzyPinyinEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path.rfind(fuzzy_prefix, 0) == 0)
|
||
return SetConfiguredFuzzyPinyinRule(path.substr(input_section_prefix.size()),
|
||
json::value_to<bool>(data.at("value")));
|
||
if (path == "appearance.tsf_preedit_style")
|
||
return SetConfiguredTsfPreeditStyle(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.ui_backend")
|
||
return SetConfiguredUiBackend(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.candidate_window_layout")
|
||
return SetConfiguredCandidateWindowLayout(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.candidate_window_follow_cursor")
|
||
return SetConfiguredCandidateWindowFollowCursor(json::value_to<bool>(data.at("value")));
|
||
if (path == "appearance.candidate_skin")
|
||
return SetConfiguredCandidateSkin(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.candidate_window_preedit_style")
|
||
return SetConfiguredCandidateWindowPreeditStyle(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.candidate_fixed_badge")
|
||
return SetConfiguredCandidateFixedBadge(json::value_to<bool>(data.at("value")));
|
||
if (path == "appearance.candidate_fixed_badge_style")
|
||
return SetConfiguredCandidateFixedBadgeStyle(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.page_size")
|
||
return SetConfiguredCandidatePageSize(static_cast<int>(data.at("value").as_int64()));
|
||
if (path == "appearance.font")
|
||
return SetConfiguredCandidateFont(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.fallback_fonts")
|
||
return SetConfiguredCandidateFallbackFonts(
|
||
json::value_to<std::vector<std::string>>(json::parse(json::value_to<std::string>(data.at("value")))));
|
||
if (path == "appearance.english_font")
|
||
return SetConfiguredCandidateEnglishFont(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.font_size")
|
||
return SetConfiguredCandidateFontSize(static_cast<int>(data.at("value").as_int64()));
|
||
if (path == "appearance.candidate_window_preedit_font_size")
|
||
return SetConfiguredCandidateWindowPreeditFontSize(static_cast<int>(data.at("value").as_int64()));
|
||
if (path == "appearance.cand_text_color")
|
||
return SetConfiguredCandidateTextColor(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_mode")
|
||
return SetConfiguredThemeMode(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_settings")
|
||
return SetConfiguredThemeSettings(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_cand")
|
||
return SetConfiguredThemeCand(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_ftb")
|
||
return SetConfiguredThemeFtb(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_menu")
|
||
return SetConfiguredThemeMenu(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_emoji")
|
||
return SetConfiguredThemeEmoji(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_screen_keyboard")
|
||
return SetConfiguredThemeScreenKeyboard(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_handwriting")
|
||
return SetConfiguredThemeHandwriting(json::value_to<std::string>(data.at("value")));
|
||
if (path == "appearance.theme_voice")
|
||
return SetConfiguredThemeVoice(json::value_to<std::string>(data.at("value")));
|
||
if (path == "general.floating_toolbar")
|
||
return SetConfiguredFloatingToolbarEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.caret_state_indicator")
|
||
return SetConfiguredCaretStateIndicatorEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.caret_state_indicator_on_focus")
|
||
return SetConfiguredCaretStateIndicatorOnFocus(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.caret_state_indicator_position")
|
||
return SetConfiguredCaretStateIndicatorPosition(json::value_to<std::string>(data.at("value")));
|
||
if (path == "general.diagnostic_log" || path == "general.candidate_window_diagnostic_log")
|
||
return SetConfiguredDiagnosticLogEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.tsf_diagnostic_log")
|
||
return SetConfiguredTsfDiagnosticLogEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "statistics.enabled")
|
||
return SetConfiguredStatisticsEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "statistics.retention")
|
||
return SetConfiguredStatisticsRetention(json::value_to<std::string>(data.at("value")));
|
||
if (path == "general.floating_toolbar_scale")
|
||
{
|
||
const json::value &value = data.at("value");
|
||
const double scale = value.is_double() ? value.as_double() : static_cast<double>(value.as_int64());
|
||
return SetConfiguredFloatingToolbarScale(scale);
|
||
}
|
||
if (path == "general.floating_toolbar_font_size")
|
||
return SetConfiguredFloatingToolbarFontSize(static_cast<int>(data.at("value").as_int64()));
|
||
constexpr std::string_view toolbar_prefix = "general.floating_toolbar_";
|
||
if (path.rfind(toolbar_prefix, 0) == 0)
|
||
return SetConfiguredFloatingToolbarItemEnabled(path.substr(toolbar_prefix.size()),
|
||
json::value_to<bool>(data.at("value")));
|
||
if (path == "general.cn_en_mixed_input")
|
||
return SetConfiguredEnglishCandidatesEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.candidate_translations")
|
||
return SetConfiguredCandidateTranslationsEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.cn_en_mixed_input_min_chars")
|
||
return SetConfiguredEnglishMixedInputMinChars(static_cast<int>(data.at("value").as_int64()));
|
||
if (path == "general.emoji_mixed_input")
|
||
return SetConfiguredEmojiMixedInputEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.kaomoji_mixed_input")
|
||
return SetConfiguredKaomojiMixedInputEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.cloud_candidates")
|
||
return SetConfiguredCloudCandidatesEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "association.sentence_wordlattice")
|
||
return SetConfiguredAssocSentenceWordLattice(json::value_to<bool>(data.at("value")));
|
||
if (path == "association.sentence_google")
|
||
return SetConfiguredAssocSentenceGoogle(json::value_to<bool>(data.at("value")));
|
||
if (path == "association.sentence_neural_desktop")
|
||
return SetConfiguredAssocSentenceNeuralDesktop(json::value_to<bool>(data.at("value")));
|
||
if (path == "association.sentence_neural_keyboard")
|
||
return SetConfiguredAssocSentenceNeuralKeyboard(json::value_to<bool>(data.at("value")));
|
||
if (path == "association.sentence_show_next_on_duplicate")
|
||
return SetConfiguredAssocSentenceShowNextOnDuplicate(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.unicode_mode")
|
||
return SetConfiguredUnicodeModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.quick_phrase")
|
||
return SetConfiguredQuickPhraseEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.date_time_mode")
|
||
return SetConfiguredDateTimeModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.emoji_mode")
|
||
return SetConfiguredEmojiModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.kaomoji_mode")
|
||
return SetConfiguredKaomojiModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.jianpin_mode")
|
||
return SetConfiguredJianpinModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.y_mode")
|
||
return SetConfiguredYModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.r_mode")
|
||
return SetConfiguredRModeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "utility.clipboard_history")
|
||
return SetConfiguredClipboardHistoryEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.paging_minus_equal")
|
||
return SetConfiguredPagingMinusEqualEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.paging_tab")
|
||
return SetConfiguredPagingTabEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.paging_comma_period")
|
||
return SetConfiguredPagingCommaPeriodEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.paging_brackets")
|
||
return SetConfiguredPagingBracketsEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.paging_page_up_down")
|
||
return SetConfiguredPagingPageUpDownEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.paging_mouse_wheel")
|
||
return SetConfiguredPagingMouseWheelEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "general.candidate_arrow_navigation")
|
||
return SetConfiguredCandidateArrowNavigationEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "keybindings.switch_language_shift")
|
||
return SetConfiguredSwitchLanguageShiftEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "keybindings.switch_language_ctrl")
|
||
return SetConfiguredSwitchLanguageCtrlEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "keybindings.switch_language_ctrl_alt_space")
|
||
return SetConfiguredSwitchLanguageCtrlAltSpaceEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "keybindings.toggle_character_set_ctrl_shift_f")
|
||
return SetConfiguredCharacterSetShortcutEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path.rfind("frequency_adjustment.", 0) == 0)
|
||
{
|
||
const std::string key = path.substr(21);
|
||
const json::value &value = data.at("value");
|
||
if (value.is_string())
|
||
return SetConfiguredFrequencyAdjustmentString(key, json::value_to<std::string>(value));
|
||
if (value.is_int64())
|
||
return SetConfiguredFrequencyAdjustmentInt(key, static_cast<int>(value.as_int64()));
|
||
return false;
|
||
}
|
||
if (path.rfind("voice_input.", 0) == 0)
|
||
{
|
||
const std::string key = path.substr(12);
|
||
const json::value &value = data.at("value");
|
||
if (value.is_bool())
|
||
{
|
||
return SetConfiguredVoiceInputBool(key, json::value_to<bool>(value));
|
||
}
|
||
if (value.is_string())
|
||
return SetConfiguredVoiceInputString(key, json::value_to<std::string>(value));
|
||
return false;
|
||
}
|
||
if (path.rfind("ai_assistant.", 0) == 0)
|
||
{
|
||
const std::string key = path.substr(13);
|
||
const json::value &value = data.at("value");
|
||
if (value.is_bool())
|
||
return SetConfiguredAiAssistantBool(key, json::value_to<bool>(value));
|
||
if (value.is_string())
|
||
return SetConfiguredAiAssistantString(key, json::value_to<std::string>(value));
|
||
if (value.is_int64())
|
||
return SetConfiguredAiAssistantInt(key, static_cast<int>(value.as_int64()));
|
||
return false;
|
||
}
|
||
if (path.rfind("tencent_tmt.", 0) == 0)
|
||
{
|
||
const json::value &value = data.at("value");
|
||
return value.is_string() && SetConfiguredTencentTmtString(path.substr(std::string("tencent_tmt.").size()),
|
||
json::value_to<std::string>(value));
|
||
}
|
||
if (path == "custom_translation.enabled")
|
||
return SetConfiguredCustomTranslationBool("enabled", json::value_to<bool>(data.at("value")));
|
||
if (path.rfind("custom_translation.", 0) == 0)
|
||
{
|
||
const json::value &value = data.at("value");
|
||
return value.is_string() &&
|
||
SetConfiguredCustomTranslationString(path.substr(std::string("custom_translation.").size()),
|
||
json::value_to<std::string>(value));
|
||
}
|
||
if (path == "niutrans.enabled")
|
||
return SetConfiguredNiuTransBool("enabled", json::value_to<bool>(data.at("value")));
|
||
if (path.rfind("niutrans.", 0) == 0)
|
||
{
|
||
const json::value &value = data.at("value");
|
||
return value.is_string() && SetConfiguredNiuTransString(path.substr(std::string("niutrans.").size()),
|
||
json::value_to<std::string>(value));
|
||
}
|
||
if (path == "helpcode.show_sp_helpcode_in_candidate_window")
|
||
return SetConfiguredShowShuangpinHelpcodeInCandidateWindow(json::value_to<bool>(data.at("value")));
|
||
if (path == "helpcode.shuangpin_helpcode")
|
||
return SetConfiguredShuangpinHelpcodeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "helpcode.shuangpin_helpcode_schema")
|
||
return SetConfiguredShuangpinHelpcodeSchema(json::value_to<std::string>(data.at("value")));
|
||
if (path == "helpcode.quanpin_helpcode")
|
||
return SetConfiguredQuanpinHelpcodeEnabled(json::value_to<bool>(data.at("value")));
|
||
if (path == "helpcode.quanpin_helpcode_schema")
|
||
return SetConfiguredQuanpinHelpcodeSchema(json::value_to<std::string>(data.at("value")));
|
||
if (path == "helpcode.show_qp_helpcode_in_candidate_window")
|
||
return SetConfiguredShowQuanpinHelpcodeInCandidateWindow(json::value_to<bool>(data.at("value")));
|
||
if (path == "quanpin.autocorrect_transposition")
|
||
return SetConfiguredQuanpinAutocorrectTransposition(json::value_to<bool>(data.at("value")));
|
||
if (path == "quanpin.autocorrect_neighbor")
|
||
return SetConfiguredQuanpinAutocorrectNeighbor(json::value_to<bool>(data.at("value")));
|
||
return false;
|
||
}
|
||
|
||
int HitTestName(const std::string &name)
|
||
{
|
||
if (name == "left")
|
||
return HTLEFT;
|
||
if (name == "right")
|
||
return HTRIGHT;
|
||
if (name == "top")
|
||
return HTTOP;
|
||
if (name == "bottom")
|
||
return HTBOTTOM;
|
||
if (name == "left-top")
|
||
return HTTOPLEFT;
|
||
if (name == "right-top")
|
||
return HTTOPRIGHT;
|
||
if (name == "left-bottom")
|
||
return HTBOTTOMLEFT;
|
||
if (name == "right-bottom")
|
||
return HTBOTTOMRIGHT;
|
||
return HTCLIENT;
|
||
}
|
||
|
||
bool CopyTextToClipboard(HWND hwnd, const std::wstring &text)
|
||
{
|
||
if (!OpenClipboard(hwnd))
|
||
return false;
|
||
EmptyClipboard();
|
||
|
||
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
|
||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||
if (!memory)
|
||
{
|
||
CloseClipboard();
|
||
return false;
|
||
}
|
||
|
||
void *destination = GlobalLock(memory);
|
||
if (!destination)
|
||
{
|
||
GlobalFree(memory);
|
||
CloseClipboard();
|
||
return false;
|
||
}
|
||
|
||
memcpy(destination, text.c_str(), bytes);
|
||
GlobalUnlock(memory);
|
||
if (!SetClipboardData(CF_UNICODETEXT, memory))
|
||
{
|
||
GlobalFree(memory);
|
||
CloseClipboard();
|
||
return false;
|
||
}
|
||
|
||
CloseClipboard();
|
||
return true;
|
||
}
|
||
|
||
std::filesystem::path StatisticsDatabasePath()
|
||
{
|
||
return std::filesystem::path(CommonUtils::get_ime_data_path_w()) / L"stats.db";
|
||
}
|
||
|
||
// The local calendar day "today" is bucketed into. GetLocalTime resolves the
|
||
// current timezone and DST, matching the Server-side aggregator's resolver.
|
||
int LocalTodayDayKey()
|
||
{
|
||
SYSTEMTIME now{};
|
||
GetLocalTime(&now);
|
||
return MsimeStats::DayKeyOf(now.wYear, now.wMonth, now.wDay);
|
||
}
|
||
|
||
json::object OverviewToJson(const MsimeStats::Overview &overview)
|
||
{
|
||
json::object categories;
|
||
categories["cjk"] = overview.categories.cjk;
|
||
categories["latin"] = overview.categories.latin;
|
||
categories["digit"] = overview.categories.digit;
|
||
categories["punct"] = overview.categories.punct;
|
||
categories["other"] = overview.categories.other;
|
||
|
||
json::array hourly;
|
||
hourly.reserve(overview.today_hourly.size());
|
||
for (const int64_t chars : overview.today_hourly)
|
||
hourly.push_back(json::value(chars));
|
||
|
||
json::array daily;
|
||
daily.reserve(overview.daily.size());
|
||
for (const MsimeStats::DailyRow &row : overview.daily)
|
||
{
|
||
json::object entry;
|
||
entry["dayKey"] = row.day_key;
|
||
entry["cjk"] = row.cjk;
|
||
entry["latin"] = row.latin;
|
||
entry["digit"] = row.digit;
|
||
entry["punct"] = row.punct;
|
||
entry["other"] = row.other;
|
||
entry["activeMs"] = row.active_ms;
|
||
daily.push_back(std::move(entry));
|
||
}
|
||
|
||
json::object result;
|
||
result["hasData"] = overview.has_data;
|
||
result["firstDayKey"] = overview.first_day_key;
|
||
result["lastDayKey"] = overview.last_day_key;
|
||
result["days"] = overview.days;
|
||
result["totalChars"] = overview.total_chars;
|
||
result["totalActiveMs"] = overview.total_active_ms;
|
||
result["todayDayKey"] = overview.today_day_key;
|
||
result["todayChars"] = overview.today_chars;
|
||
result["todayActiveMs"] = overview.today_active_ms;
|
||
result["averagePerDay"] = overview.average_per_day;
|
||
result["currentStreak"] = overview.current_streak;
|
||
result["longestStreak"] = overview.longest_streak;
|
||
result["bestDayKey"] = overview.best_day_key;
|
||
result["bestDayChars"] = overview.best_day_chars;
|
||
result["todaySpeed"] = overview.today_speed;
|
||
result["averageSpeed"] = overview.average_speed;
|
||
result["fastestSpeed"] = overview.fastest_speed;
|
||
result["fastestDayKey"] = overview.fastest_day_key;
|
||
result["categories"] = std::move(categories);
|
||
result["todayHourly"] = std::move(hourly);
|
||
result["daily"] = std::move(daily);
|
||
return result;
|
||
}
|
||
|
||
// Handles one statistics request on the worker thread. Each request opens the
|
||
// store, uses it and closes it: the settings process can exit at any time, the
|
||
// store carries no cross-request state, and all requests are serialised by the
|
||
// single serial task queue, so no in-process lock is needed. WAL plus the
|
||
// store's busy timeout arbitrate with the Server process that writes the same
|
||
// file.
|
||
json::object HandleStatsRequest(const json::object &data, HWND hwnd)
|
||
{
|
||
const std::string request_id = json::value_to<std::string>(data.at("requestId"));
|
||
const std::string action = json::value_to<std::string>(data.at("action"));
|
||
json::object response;
|
||
response["requestId"] = request_id;
|
||
response["action"] = action;
|
||
response["ok"] = true;
|
||
response["message"] = "成功";
|
||
|
||
if (action == "openDirectory")
|
||
{
|
||
const std::filesystem::path directory = StatisticsDatabasePath().parent_path();
|
||
std::error_code error_code;
|
||
std::filesystem::create_directories(directory, error_code);
|
||
ShellExecuteW(hwnd, L"open", directory.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||
response["message"] = "已打开数据目录";
|
||
return response;
|
||
}
|
||
if (action != "overview" && action != "clearAll")
|
||
{
|
||
response["ok"] = false;
|
||
response["message"] = "未知的统计操作。";
|
||
return response;
|
||
}
|
||
|
||
// The missing-file check is the observable half of "recording is off by
|
||
// default": Store::Open always opens with SQLITE_OPEN_CREATE, so merely
|
||
// browsing statistics must not create stats.db.
|
||
std::error_code exists_error;
|
||
const std::filesystem::path database_path = StatisticsDatabasePath();
|
||
const bool database_exists = std::filesystem::exists(database_path, exists_error) && !exists_error;
|
||
std::unique_ptr<MsimeStats::Store> store;
|
||
if (database_exists)
|
||
{
|
||
std::string error;
|
||
store = MsimeStats::Store::Open(database_path, error);
|
||
if (!store)
|
||
{
|
||
response["ok"] = false;
|
||
response["message"] = "无法打开统计数据库:" + error;
|
||
return response;
|
||
}
|
||
}
|
||
|
||
const int today = LocalTodayDayKey();
|
||
if (action == "clearAll")
|
||
{
|
||
int64_t removed_days = 0;
|
||
if (store)
|
||
{
|
||
std::string error;
|
||
if (!store->ClearAll(removed_days, error))
|
||
{
|
||
response["ok"] = false;
|
||
response["message"] = "清空统计数据失败:" + error;
|
||
return response;
|
||
}
|
||
}
|
||
response["removedDays"] = removed_days;
|
||
response["message"] =
|
||
removed_days > 0 ? "已清空 " + std::to_string(removed_days) + " 天数据" : "没有需要清空的数据";
|
||
}
|
||
|
||
MsimeStats::Overview overview;
|
||
if (store)
|
||
{
|
||
std::vector<MsimeStats::DailyRow> daily;
|
||
std::vector<MsimeStats::HourlyRow> hourly;
|
||
std::string error;
|
||
if (!store->DailyRows(daily, error) || !store->HourlyRows(today, hourly, error))
|
||
{
|
||
response["ok"] = false;
|
||
response["message"] = "读取统计数据失败:" + error;
|
||
return response;
|
||
}
|
||
overview = MsimeStats::ComputeOverview(daily, hourly, today);
|
||
}
|
||
else
|
||
{
|
||
// Derive the empty overview through the same path so the JSON shape is
|
||
// identical (24 zeroed hour buckets, zeroed categories).
|
||
overview = MsimeStats::ComputeOverview({}, {}, today);
|
||
}
|
||
response["overview"] = OverviewToJson(overview);
|
||
if (action == "overview")
|
||
response["message"] = "已加载统计数据";
|
||
return response;
|
||
}
|
||
|
||
void HandleWebMessage(HWND hwnd, ICoreWebView2WebMessageReceivedEventArgs *args)
|
||
{
|
||
if (g_closing || !g_worker)
|
||
return;
|
||
wil::unique_cotaskmem_string raw;
|
||
if (FAILED(args->TryGetWebMessageAsString(&raw)) || !raw)
|
||
return;
|
||
|
||
try
|
||
{
|
||
json::value value = json::parse(wstring_to_string(raw.get()));
|
||
if (!metasequoia::webview::Validate(value, "client", "settings"))
|
||
return;
|
||
const std::string type = json::value_to<std::string>(value.at("type"));
|
||
if (type == "dragStart")
|
||
{
|
||
ReleaseCapture();
|
||
PostMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||
}
|
||
else if (type == "resizeHitTest" || type == "resizeStart")
|
||
{
|
||
const int hit_test = HitTestName(json::value_to<std::string>(value.at("data")));
|
||
if (hit_test != HTCLIENT)
|
||
{
|
||
ReleaseCapture();
|
||
PostMessageW(hwnd, WM_NCLBUTTONDOWN, hit_test, 0);
|
||
}
|
||
}
|
||
else if (type == "focus")
|
||
{
|
||
SetFocus(hwnd);
|
||
}
|
||
else if (type == "windowControl")
|
||
{
|
||
const std::string command = json::value_to<std::string>(value.at("data"));
|
||
if (command == "minimize")
|
||
ShowWindow(hwnd, SW_MINIMIZE);
|
||
else if (command == "maximize")
|
||
ShowWindow(hwnd, SW_MAXIMIZE);
|
||
else if (command == "restore")
|
||
ShowWindow(hwnd, SW_RESTORE);
|
||
else if (command == "close")
|
||
HideSettings(hwnd);
|
||
}
|
||
else if (type == "maximizeButtonRect")
|
||
{
|
||
const auto &data = value.at("data").as_object();
|
||
const double x = data.if_contains("x") ? json::value_to<double>(*data.if_contains("x")) : 0.0;
|
||
const double y = data.if_contains("y") ? json::value_to<double>(*data.if_contains("y")) : 0.0;
|
||
const double width = data.if_contains("width") ? json::value_to<double>(*data.if_contains("width")) : 0.0;
|
||
const double height =
|
||
data.if_contains("height") ? json::value_to<double>(*data.if_contains("height")) : 0.0;
|
||
double scale = data.if_contains("dpr") ? json::value_to<double>(*data.if_contains("dpr")) : 0.0;
|
||
if (scale <= 0.0)
|
||
scale = static_cast<double>(GetDpiForWindow(hwnd)) / 96.0;
|
||
g_maximize_button_rect = {static_cast<LONG>(std::lround(x * scale)),
|
||
static_cast<LONG>(std::lround(y * scale)),
|
||
static_cast<LONG>(std::lround((x + width) * scale)),
|
||
static_cast<LONG>(std::lround((y + height) * scale))};
|
||
g_has_maximize_button_rect = true;
|
||
}
|
||
else if (type == "configRequest")
|
||
{
|
||
// A snapshot sent before the new document installed its listener may be lost.
|
||
g_last_config_message.clear();
|
||
PostConfig(false);
|
||
PostMessageW(hwnd, kScanSkinCatalog, 0, 0);
|
||
}
|
||
else if (type == "skinCatalogRequest")
|
||
{
|
||
g_worker->Submit([] {
|
||
auto completion = ConfigCompletion(true);
|
||
NotifyImeServerCandidateSkinRefresh();
|
||
return completion;
|
||
});
|
||
}
|
||
else if (type == "openSkinDirectory")
|
||
{
|
||
const std::filesystem::path skins = std::filesystem::path(CommonUtils::get_ime_data_path_w()) / L"skins";
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(skins, ec);
|
||
if (!ec)
|
||
ShellExecuteW(hwnd, L"open", skins.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||
}
|
||
else if (type == "openHelpcodeDirectory")
|
||
{
|
||
const std::filesystem::path directory = metasequoia::path_from_utf8(GetCustomHelpcodeDirectory().c_str());
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(directory, ec);
|
||
if (!ec)
|
||
ShellExecuteW(hwnd, L"open", directory.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||
}
|
||
else if (type == "configUpdate")
|
||
{
|
||
const auto data = value.at("data").as_object();
|
||
g_worker->Submit([data] {
|
||
const bool saved = ApplyConfigUpdate(data);
|
||
auto completion = ConfigCompletion();
|
||
return [saved, completion = std::move(completion)] {
|
||
completion();
|
||
if (!saved)
|
||
MessageBoxW(g_settings_hwnd, L"设置保存失败,请重试。", L"水杉输入法", MB_OK | MB_ICONWARNING);
|
||
};
|
||
});
|
||
}
|
||
else if (type == "apiCredentialTest")
|
||
{
|
||
const auto &data = value.at("data").as_object();
|
||
const std::string request_id = json::value_to<std::string>(data.at("requestId"));
|
||
ApiCredentialTest::Request request;
|
||
request.service = json::value_to<std::string>(data.at("service"));
|
||
for (const auto &[key, item] : data.at("config").as_object())
|
||
{
|
||
if (item.is_string())
|
||
request.config.emplace(std::string(key), json::value_to<std::string>(item));
|
||
}
|
||
g_worker->Submit([request_id, request = std::move(request)]() -> SerialTaskQueue::Completion {
|
||
ApiCredentialTest::Result result;
|
||
try
|
||
{
|
||
result = ApiCredentialTest::Run(request);
|
||
}
|
||
catch (...)
|
||
{
|
||
result = {false, "测试失败:内部错误。"};
|
||
}
|
||
json::value response = {{"type", "apiCredentialTestResult"},
|
||
{"requestId", request_id},
|
||
{"ok", result.ok},
|
||
{"message", result.message},
|
||
{"protocolVersion", metasequoia::webview::Version}};
|
||
if (!metasequoia::webview::Validate(response, "server"))
|
||
throw std::runtime_error("Invalid API credential test response");
|
||
auto message = string_to_wstring(json::serialize(response));
|
||
return [message = std::move(message)] {
|
||
if (g_webview)
|
||
g_webview->PostWebMessageAsJson(message.c_str());
|
||
};
|
||
});
|
||
}
|
||
else if (type == "openKeyboardPanel")
|
||
{
|
||
OpenKeyboardPanelApplication();
|
||
}
|
||
else if (type == "openHandwritingPanel")
|
||
{
|
||
OpenHandwritingPanelApplication();
|
||
}
|
||
else if (type == "restartServer")
|
||
{
|
||
if (!NotifyImeServerRestart())
|
||
{
|
||
MessageBoxW(g_settings_hwnd, L"未能重启输入法进程,请稍后重试。", L"水杉输入法",
|
||
MB_OK | MB_ICONWARNING);
|
||
}
|
||
}
|
||
else if (type == "openExternalUrl")
|
||
{
|
||
const std::string url = json::value_to<std::string>(value.at("data"));
|
||
if (url.rfind("https://", 0) == 0)
|
||
ShellExecuteW(hwnd, L"open", string_to_wstring(url).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||
}
|
||
else if (type == "copyText")
|
||
{
|
||
CopyTextToClipboard(hwnd, string_to_wstring(json::value_to<std::string>(value.at("data"))));
|
||
}
|
||
else if (type == "dictionaryRequest")
|
||
{
|
||
const auto data = value.at("data").as_object();
|
||
g_worker->Submit([data]() -> SerialTaskQueue::Completion {
|
||
json::value response;
|
||
try
|
||
{
|
||
response = SettingsDictionary::HandleRequest(data);
|
||
}
|
||
catch (...)
|
||
{
|
||
response =
|
||
json::object{{"ok", false}, {"message", "词库操作失败,请重试"}, {"rows", json::array{}}};
|
||
}
|
||
auto &object = response.as_object();
|
||
object["type"] = "dictionaryResponse";
|
||
object["requestId"] = data.at("requestId");
|
||
object["action"] = data.at("action");
|
||
object["dictionary"] = data.at("dictionary");
|
||
object["protocolVersion"] = metasequoia::webview::Version;
|
||
if (!metasequoia::webview::Validate(response, "server"))
|
||
throw std::runtime_error("Invalid dictionary response");
|
||
auto message = string_to_wstring(json::serialize(response));
|
||
return [message = std::move(message)] {
|
||
if (g_webview)
|
||
g_webview->PostWebMessageAsJson(message.c_str());
|
||
};
|
||
});
|
||
}
|
||
else if (type == "statsRequest")
|
||
{
|
||
const auto data = value.at("data").as_object();
|
||
g_worker->Submit([data, hwnd]() -> SerialTaskQueue::Completion {
|
||
json::object response;
|
||
try
|
||
{
|
||
response = HandleStatsRequest(data, hwnd);
|
||
}
|
||
catch (...)
|
||
{
|
||
response["ok"] = false;
|
||
response["message"] = "统计操作失败,请重试";
|
||
}
|
||
response["requestId"] = data.at("requestId");
|
||
response["action"] = data.at("action");
|
||
response["type"] = "statsResponse";
|
||
response["protocolVersion"] = metasequoia::webview::Version;
|
||
if (!metasequoia::webview::Validate(response, "server"))
|
||
throw std::runtime_error("Invalid statistics response");
|
||
auto message = string_to_wstring(json::serialize(response));
|
||
return [message = std::move(message)] {
|
||
if (g_webview)
|
||
g_webview->PostWebMessageAsJson(message.c_str());
|
||
};
|
||
});
|
||
}
|
||
}
|
||
catch (const std::exception &)
|
||
{
|
||
// Ignore malformed messages from the page; the next snapshot restores its state.
|
||
}
|
||
}
|
||
|
||
HRESULT EnsureCompositionTree(HWND hwnd)
|
||
{
|
||
HRESULT hr = DCompositionCreateDevice(nullptr, __uuidof(IDCompositionDevice),
|
||
reinterpret_cast<void **>(g_dcomp_device.GetAddressOf()));
|
||
if (FAILED(hr))
|
||
return hr;
|
||
hr = g_dcomp_device->CreateTargetForHwnd(hwnd, TRUE, &g_dcomp_target);
|
||
if (FAILED(hr))
|
||
return hr;
|
||
hr = g_dcomp_device->CreateVisual(&g_dcomp_root);
|
||
if (FAILED(hr))
|
||
return hr;
|
||
hr = g_dcomp_target->SetRoot(g_dcomp_root.Get());
|
||
if (FAILED(hr))
|
||
return hr;
|
||
return g_dcomp_device->Commit();
|
||
}
|
||
|
||
HRESULT OnControllerCreated(HWND hwnd, HRESULT result, ICoreWebView2CompositionController *controller)
|
||
{
|
||
if (FAILED(result) || !controller)
|
||
{
|
||
SettingsSplash::Dismiss();
|
||
return FAILED(result) ? result : E_FAIL;
|
||
}
|
||
g_composition_controller = controller;
|
||
if (FAILED(g_composition_controller.As(&g_controller)))
|
||
return E_NOINTERFACE;
|
||
if (FAILED(g_controller->get_CoreWebView2(&g_webview)) || !g_webview)
|
||
return E_FAIL;
|
||
|
||
ComPtr<ICoreWebView2Settings> settings;
|
||
if (SUCCEEDED(g_webview->get_Settings(&settings)))
|
||
{
|
||
settings->put_IsScriptEnabled(TRUE);
|
||
settings->put_AreDefaultScriptDialogsEnabled(TRUE);
|
||
settings->put_IsWebMessageEnabled(TRUE);
|
||
settings->put_AreHostObjectsAllowed(TRUE);
|
||
settings->put_IsZoomControlEnabled(FALSE);
|
||
settings->put_IsStatusBarEnabled(FALSE);
|
||
}
|
||
g_controller->put_ZoomFactor(1.0);
|
||
|
||
if (SUCCEEDED(g_webview.As(&g_webview3)))
|
||
{
|
||
const std::filesystem::path assets =
|
||
std::filesystem::path(CommonUtils::get_ime_data_path_w()) / "html/webview2/settings/ime-settings/dist";
|
||
g_webview3->SetVirtualHostNameToFolderMapping(L"imesettings", assets.c_str(),
|
||
COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW);
|
||
const std::filesystem::path skins = std::filesystem::path(CommonUtils::get_ime_data_path_w()) / L"skins";
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(skins, ec);
|
||
g_webview3->SetVirtualHostNameToFolderMapping(L"candidate-skins", skins.c_str(),
|
||
COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW);
|
||
}
|
||
if (SUCCEEDED(g_controller.As(&g_controller2)))
|
||
{
|
||
ApplySettingsChromeTheme(hwnd);
|
||
}
|
||
|
||
HRESULT hr = EnsureCompositionTree(hwnd);
|
||
if (FAILED(hr))
|
||
return hr;
|
||
hr = g_composition_controller->put_RootVisualTarget(g_dcomp_root.Get());
|
||
if (FAILED(hr))
|
||
return hr;
|
||
g_dcomp_device->Commit();
|
||
|
||
RECT bounds{};
|
||
GetClientRect(hwnd, &bounds);
|
||
g_controller->put_Bounds(bounds);
|
||
|
||
EventRegistrationToken token{};
|
||
g_webview->add_ContentLoading(
|
||
Callback<ICoreWebView2ContentLoadingEventHandler>([](ICoreWebView2 *,
|
||
ICoreWebView2ContentLoadingEventArgs *) -> HRESULT {
|
||
g_last_config_message.clear();
|
||
g_webview_content_started = true;
|
||
SettingsSplash::Dismiss();
|
||
return S_OK;
|
||
}).Get(),
|
||
&token);
|
||
g_webview->add_NavigationCompleted(
|
||
Callback<ICoreWebView2NavigationCompletedEventHandler>([hwnd](ICoreWebView2 *,
|
||
ICoreWebView2NavigationCompletedEventArgs *args)
|
||
-> HRESULT {
|
||
BOOL success = FALSE;
|
||
args->get_IsSuccess(&success);
|
||
SettingsSplash::Dismiss();
|
||
if (success)
|
||
{
|
||
PostWindowState(hwnd);
|
||
PostConfig();
|
||
ApplyWindowActivationAppearance();
|
||
SetFocus(hwnd);
|
||
if (g_open_about_on_ready)
|
||
{
|
||
ShowAboutSection();
|
||
g_open_about_on_ready = false;
|
||
}
|
||
}
|
||
return S_OK;
|
||
}).Get(),
|
||
&token);
|
||
g_webview->add_WebMessageReceived(
|
||
Callback<ICoreWebView2WebMessageReceivedEventHandler>([hwnd](ICoreWebView2 *,
|
||
ICoreWebView2WebMessageReceivedEventArgs *args)
|
||
-> HRESULT {
|
||
HandleWebMessage(hwnd, args);
|
||
return S_OK;
|
||
}).Get(),
|
||
&token);
|
||
return g_webview->Navigate(L"https://imesettings/index.html");
|
||
}
|
||
|
||
void InitWebView(HWND hwnd)
|
||
{
|
||
std::filesystem::path user_data = CommonUtils::get_webview2_user_data_path(L"webview2-settings");
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(user_data, ec);
|
||
auto options = Make<CoreWebView2EnvironmentOptions>();
|
||
options->put_AdditionalBrowserArguments(L"--disable-features=TranslateUI "
|
||
L"--disable-background-networking "
|
||
L"--disable-default-apps "
|
||
L"--disable-sync "
|
||
L"--disable-prompt-on-repost "
|
||
L"--no-first-run");
|
||
CreateCoreWebView2EnvironmentWithOptions(
|
||
nullptr, user_data.c_str(), options.Get(),
|
||
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
|
||
[hwnd](HRESULT result, ICoreWebView2Environment *environment) -> HRESULT {
|
||
if (FAILED(result) || !environment)
|
||
{
|
||
SettingsSplash::Dismiss();
|
||
return FAILED(result) ? result : E_FAIL;
|
||
}
|
||
ComPtr<ICoreWebView2Environment3> environment3;
|
||
if (FAILED(environment->QueryInterface(IID_PPV_ARGS(&environment3))))
|
||
{
|
||
SettingsSplash::Dismiss();
|
||
return E_NOINTERFACE;
|
||
}
|
||
return environment3->CreateCoreWebView2CompositionController(
|
||
hwnd,
|
||
Callback<ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler>(
|
||
[hwnd](HRESULT controller_result, ICoreWebView2CompositionController *controller) -> HRESULT {
|
||
return OnControllerCreated(hwnd, controller_result, controller);
|
||
})
|
||
.Get());
|
||
})
|
||
.Get());
|
||
}
|
||
|
||
UINT32 MouseKeys(WPARAM w_param)
|
||
{
|
||
UINT32 keys = COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE;
|
||
if (w_param & MK_LBUTTON)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON;
|
||
if (w_param & MK_RBUTTON)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_RIGHT_BUTTON;
|
||
if (w_param & MK_MBUTTON)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_MIDDLE_BUTTON;
|
||
if (w_param & MK_SHIFT)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_SHIFT;
|
||
if (w_param & MK_CONTROL)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_CONTROL;
|
||
if (w_param & MK_XBUTTON1)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_X_BUTTON1;
|
||
if (w_param & MK_XBUTTON2)
|
||
keys |= COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_X_BUTTON2;
|
||
return keys;
|
||
}
|
||
|
||
bool ForwardMouse(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param)
|
||
{
|
||
if (!g_composition_controller)
|
||
return false;
|
||
COREWEBVIEW2_MOUSE_EVENT_KIND kind{};
|
||
UINT32 data = 0;
|
||
switch (message)
|
||
{
|
||
case WM_MOUSEMOVE:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE;
|
||
break;
|
||
case WM_LBUTTONDOWN:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN;
|
||
SetFocus(hwnd);
|
||
break;
|
||
case WM_LBUTTONUP:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP;
|
||
break;
|
||
case WM_LBUTTONDBLCLK:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOUBLE_CLICK;
|
||
break;
|
||
case WM_RBUTTONDOWN:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN;
|
||
break;
|
||
case WM_RBUTTONUP:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP;
|
||
break;
|
||
case WM_RBUTTONDBLCLK:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOUBLE_CLICK;
|
||
break;
|
||
case WM_MBUTTONDOWN:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN;
|
||
break;
|
||
case WM_MBUTTONUP:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP;
|
||
break;
|
||
case WM_MBUTTONDBLCLK:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOUBLE_CLICK;
|
||
break;
|
||
case WM_XBUTTONDOWN:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_DOWN;
|
||
data = GET_XBUTTON_WPARAM(w_param);
|
||
break;
|
||
case WM_XBUTTONUP:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_UP;
|
||
data = GET_XBUTTON_WPARAM(w_param);
|
||
break;
|
||
case WM_XBUTTONDBLCLK:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_X_BUTTON_DOUBLE_CLICK;
|
||
data = GET_XBUTTON_WPARAM(w_param);
|
||
break;
|
||
case WM_MOUSEWHEEL:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL;
|
||
data = GET_WHEEL_DELTA_WPARAM(w_param);
|
||
break;
|
||
case WM_MOUSEHWHEEL:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL;
|
||
data = GET_WHEEL_DELTA_WPARAM(w_param);
|
||
break;
|
||
case WM_MOUSELEAVE:
|
||
kind = COREWEBVIEW2_MOUSE_EVENT_KIND_LEAVE;
|
||
break;
|
||
default:
|
||
return false;
|
||
}
|
||
POINT point{GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param)};
|
||
if (message == WM_MOUSEWHEEL || message == WM_MOUSEHWHEEL)
|
||
ScreenToClient(hwnd, &point);
|
||
if (message == WM_MOUSEMOVE)
|
||
{
|
||
TRACKMOUSEEVENT track{sizeof(track), TME_LEAVE, hwnd, 0};
|
||
TrackMouseEvent(&track);
|
||
}
|
||
g_composition_controller->SendMouseInput(
|
||
kind, static_cast<COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS>(MouseKeys(w_param)), data, point);
|
||
return true;
|
||
}
|
||
|
||
bool IsMaximizeButton(HWND hwnd, POINT screen_point)
|
||
{
|
||
if (!g_has_maximize_button_rect)
|
||
return false;
|
||
ScreenToClient(hwnd, &screen_point);
|
||
return PtInRect(&g_maximize_button_rect, screen_point) != FALSE;
|
||
}
|
||
|
||
int TopNonClientInset(HWND hwnd)
|
||
{
|
||
const UINT dpi = GetDpiForWindow(hwnd);
|
||
const DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hwnd, GWL_STYLE));
|
||
const DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hwnd, GWL_EXSTYLE));
|
||
RECT caption{}, no_caption{};
|
||
AdjustWindowRectExForDpi(&caption, style, FALSE, ex_style, dpi);
|
||
AdjustWindowRectExForDpi(&no_caption, style & ~WS_CAPTION, FALSE, ex_style, dpi);
|
||
return no_caption.top - caption.top + GetSystemMetricsForDpi(SM_CYSIZEFRAME, dpi) +
|
||
GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
|
||
}
|
||
|
||
// 首帧之前隐藏 WebView2 会卡住 raster 初始化,预热阶段不要碰可见性。
|
||
void SetWebViewVisible(bool visible)
|
||
{
|
||
if (g_controller && g_webview_content_started)
|
||
g_controller->put_IsVisible(visible ? TRUE : FALSE);
|
||
}
|
||
|
||
// 重新露面:取消延迟退出,恢复渲染与配置轮询。
|
||
void CancelLinger(HWND hwnd)
|
||
{
|
||
if (!g_lingering)
|
||
return;
|
||
g_lingering = false;
|
||
KillTimer(hwnd, kLingerTimer);
|
||
SetWebViewVisible(true);
|
||
if (g_webview3)
|
||
g_webview3->Resume();
|
||
SetTimer(hwnd, kConfigReloadTimer, 300, nullptr);
|
||
}
|
||
|
||
void ActivateWindow(HWND hwnd)
|
||
{
|
||
if (g_closing)
|
||
return;
|
||
CancelLinger(hwnd);
|
||
if (IsIconic(hwnd))
|
||
ShowWindow(hwnd, SW_RESTORE);
|
||
else
|
||
ShowWindow(hwnd, SW_SHOW);
|
||
SetForegroundWindow(hwnd);
|
||
SetFocus(hwnd);
|
||
}
|
||
|
||
// 用户关闭窗口时走这里:只隐藏,进程继续持有导航完成的 WebView2,kLingerTimeoutMs
|
||
// 之后才真正退出。隐藏期间停掉配置轮询并让 controller 不可见,避免白拿 CPU。
|
||
// 首帧还没出来就被关掉,说明用户不想等这次冷启动,直接退出,别留一个半初始化的进程。
|
||
void HideSettings(HWND hwnd)
|
||
{
|
||
if (g_closing)
|
||
return;
|
||
if (!g_webview_content_started)
|
||
{
|
||
CloseSettings(hwnd);
|
||
return;
|
||
}
|
||
if (g_lingering)
|
||
{
|
||
ShowWindow(hwnd, SW_HIDE);
|
||
return;
|
||
}
|
||
g_lingering = true;
|
||
// 窗口隐藏时保留的那一帧会在下次打开时先显示出来,带着高亮的标题栏按钮就会闪一下。
|
||
// 页面自己的关闭按钮已经在点击时清过,这里覆盖 Alt+F4、启动图的关闭按钮等不经过页面的路径。
|
||
ResetTitlebarHoverAfterVisibilityChange();
|
||
// 隐藏的窗口收不到 WM_NCMOUSELEAVE,本地这份悬停状态要手动清掉,
|
||
// 否则下次真的移进去时不会再发 enter。
|
||
g_maximize_button_hover = false;
|
||
KillTimer(hwnd, kConfigReloadTimer);
|
||
SettingsSplash::Dismiss();
|
||
ShowWindow(hwnd, SW_HIDE);
|
||
SetWebViewVisible(false);
|
||
SetTimer(hwnd, kLingerTimer, kLingerTimeoutMs, nullptr);
|
||
}
|
||
|
||
void CloseSettings(HWND hwnd)
|
||
{
|
||
if (g_closing)
|
||
return;
|
||
g_closing = true;
|
||
g_lingering = false;
|
||
KillTimer(hwnd, kLingerTimer);
|
||
KillTimer(hwnd, kConfigReloadTimer);
|
||
SettingsSplash::Dismiss();
|
||
ShowWindow(hwnd, SW_HIDE);
|
||
if (g_worker)
|
||
g_worker->Stop();
|
||
else
|
||
DestroyWindow(hwnd);
|
||
}
|
||
|
||
LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param)
|
||
{
|
||
switch (message)
|
||
{
|
||
case kActivateExistingWindow:
|
||
ActivateWindow(hwnd);
|
||
return 0;
|
||
case kOpenAboutSection:
|
||
ActivateWindow(hwnd);
|
||
ShowAboutSection();
|
||
return 0;
|
||
case kScanSkinCatalog:
|
||
PostConfig(true);
|
||
return 0;
|
||
case WM_NCCALCSIZE:
|
||
if (w_param)
|
||
{
|
||
auto *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(l_param);
|
||
const LRESULT result = DefWindowProcW(hwnd, message, w_param, l_param);
|
||
params->rgrc[0].top -= IsZoomed(hwnd) ? GetSystemMetrics(SM_CYCAPTION) : TopNonClientInset(hwnd);
|
||
return result;
|
||
}
|
||
break;
|
||
case WM_NCHITTEST: {
|
||
const LRESULT result = DefWindowProcW(hwnd, message, w_param, l_param);
|
||
POINT point{GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param)};
|
||
return result == HTCLIENT && IsMaximizeButton(hwnd, point) ? HTMAXBUTTON : result;
|
||
}
|
||
case WM_MOVE:
|
||
case WM_MOVING:
|
||
if (g_controller)
|
||
g_controller->NotifyParentWindowPositionChanged();
|
||
if (SettingsSplash::IsVisible())
|
||
SettingsSplash::SyncToOwner();
|
||
break;
|
||
case WM_SETFOCUS:
|
||
if (g_controller)
|
||
g_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
|
||
break;
|
||
case WM_ACTIVATE: {
|
||
const bool active = LOWORD(w_param) != WA_INACTIVE;
|
||
if (g_window_active != active)
|
||
{
|
||
g_window_active = active;
|
||
ApplyWindowActivationAppearance();
|
||
}
|
||
if (active)
|
||
{
|
||
if (g_webview3)
|
||
g_webview3->Resume();
|
||
if (g_controller)
|
||
g_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
|
||
}
|
||
break;
|
||
}
|
||
case WM_MOUSEMOVE:
|
||
case WM_MOUSELEAVE:
|
||
case WM_LBUTTONDOWN:
|
||
case WM_LBUTTONUP:
|
||
case WM_LBUTTONDBLCLK:
|
||
case WM_RBUTTONDOWN:
|
||
case WM_RBUTTONUP:
|
||
case WM_RBUTTONDBLCLK:
|
||
case WM_MBUTTONDOWN:
|
||
case WM_MBUTTONUP:
|
||
case WM_MBUTTONDBLCLK:
|
||
case WM_XBUTTONDOWN:
|
||
case WM_XBUTTONUP:
|
||
case WM_XBUTTONDBLCLK:
|
||
case WM_MOUSEWHEEL:
|
||
case WM_MOUSEHWHEEL:
|
||
if (ForwardMouse(hwnd, message, w_param, l_param))
|
||
return (message == WM_XBUTTONDOWN || message == WM_XBUTTONUP || message == WM_XBUTTONDBLCLK) ? TRUE : 0;
|
||
break;
|
||
case WM_SETCURSOR:
|
||
if (g_composition_controller && LOWORD(l_param) == HTCLIENT)
|
||
{
|
||
UINT32 cursor = 0;
|
||
if (SUCCEEDED(g_composition_controller->get_SystemCursorId(&cursor)) && cursor)
|
||
{
|
||
SetCursor(LoadCursorW(nullptr, MAKEINTRESOURCEW(cursor)));
|
||
return TRUE;
|
||
}
|
||
}
|
||
break;
|
||
case WM_NCMOUSEMOVE: {
|
||
POINT point{GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param)};
|
||
if (IsMaximizeButton(hwnd, point))
|
||
{
|
||
if (!g_maximize_button_hover)
|
||
{
|
||
g_maximize_button_hover = true;
|
||
PostMaximizeButtonEvent("enter");
|
||
}
|
||
TRACKMOUSEEVENT track{sizeof(track), TME_LEAVE | TME_NONCLIENT, hwnd, 0};
|
||
TrackMouseEvent(&track);
|
||
return 0;
|
||
}
|
||
break;
|
||
}
|
||
case WM_NCMOUSELEAVE:
|
||
if (g_maximize_button_hover)
|
||
{
|
||
g_maximize_button_hover = false;
|
||
PostMaximizeButtonEvent("leave");
|
||
}
|
||
break;
|
||
case WM_NCLBUTTONDOWN:
|
||
case WM_NCLBUTTONUP: {
|
||
POINT point{GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param)};
|
||
if (IsMaximizeButton(hwnd, point))
|
||
{
|
||
PostMaximizeButtonEvent(message == WM_NCLBUTTONDOWN ? "down" : "up");
|
||
return 0;
|
||
}
|
||
break;
|
||
}
|
||
case WM_ERASEBKGND: {
|
||
RECT client{};
|
||
GetClientRect(hwnd, &client);
|
||
const bool light = g_settings_light;
|
||
HBRUSH brush = CreateSolidBrush(light ? RGB(243, 243, 243) : RGB(32, 32, 32));
|
||
FillRect(reinterpret_cast<HDC>(w_param), &client, brush);
|
||
DeleteObject(brush);
|
||
return 1;
|
||
}
|
||
case WM_GETMINMAXINFO: {
|
||
auto *info = reinterpret_cast<MINMAXINFO *>(l_param);
|
||
const UINT dpi = GetDpiForWindow(hwnd);
|
||
info->ptMinTrackSize = {MulDiv(600, dpi, 96), MulDiv(400, dpi, 96)};
|
||
return 0;
|
||
}
|
||
case WM_SIZE: {
|
||
const bool minimized = w_param == SIZE_MINIMIZED;
|
||
if (minimized || g_window_minimized)
|
||
ResetTitlebarHoverAfterVisibilityChange();
|
||
g_window_minimized = minimized;
|
||
if (g_controller)
|
||
{
|
||
RECT bounds{};
|
||
GetClientRect(hwnd, &bounds);
|
||
g_controller->put_Bounds(bounds);
|
||
PostWindowState(hwnd);
|
||
}
|
||
if (SettingsSplash::IsVisible())
|
||
SettingsSplash::SyncToOwner();
|
||
break;
|
||
}
|
||
case WM_TIMER:
|
||
if (w_param == kLingerTimer)
|
||
{
|
||
CloseSettings(hwnd);
|
||
return 0;
|
||
}
|
||
if (w_param == kConfigReloadTimer && g_worker && !g_closing && !g_reload_pending)
|
||
{
|
||
g_reload_pending = true;
|
||
g_worker->Submit([] {
|
||
SerialTaskQueue::Completion completion;
|
||
try
|
||
{
|
||
if (ReloadImeConfigIfChanged())
|
||
completion = ConfigCompletion();
|
||
}
|
||
catch (...)
|
||
{ /* Retry on the next timer tick; do not poison the queue. */
|
||
}
|
||
return [completion = std::move(completion)] {
|
||
g_reload_pending = false;
|
||
if (completion)
|
||
completion();
|
||
};
|
||
});
|
||
}
|
||
return 0;
|
||
case kWorkerCompleted:
|
||
if (g_worker)
|
||
{
|
||
g_worker->Drain(!g_closing);
|
||
if (g_closing && g_worker->Stopped())
|
||
DestroyWindow(hwnd);
|
||
}
|
||
return 0;
|
||
case WM_CLOSE:
|
||
HideSettings(hwnd);
|
||
return 0;
|
||
case kQuitSettings:
|
||
CloseSettings(hwnd);
|
||
return 0;
|
||
case WM_DESTROY:
|
||
g_closing = true;
|
||
g_lingering = false;
|
||
if (g_worker)
|
||
g_worker->Stop();
|
||
KillTimer(hwnd, kLingerTimer);
|
||
KillTimer(hwnd, kConfigReloadTimer);
|
||
SettingsSplash::Dismiss();
|
||
g_webview.Reset();
|
||
g_controller2.Reset();
|
||
g_controller.Reset();
|
||
g_composition_controller.Reset();
|
||
g_dcomp_root.Reset();
|
||
g_dcomp_target.Reset();
|
||
g_dcomp_device.Reset();
|
||
PostQuitMessage(0);
|
||
return 0;
|
||
}
|
||
return DefWindowProcW(hwnd, message, w_param, l_param);
|
||
}
|
||
} // namespace
|
||
|
||
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR command_line, int show_command)
|
||
{
|
||
g_open_about_on_ready = command_line && std::wstring(command_line).find(L"--about") != std::wstring::npos;
|
||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||
const HRESULT com_result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||
if (FAILED(com_result))
|
||
return 1;
|
||
|
||
CommonUtils::SingleInstanceGuard single_instance(kSingleInstanceMutex);
|
||
if (!single_instance.is_valid())
|
||
return 1;
|
||
if (single_instance.already_running())
|
||
{
|
||
HWND existing = FindWindowW(kWindowClass, nullptr);
|
||
if (existing)
|
||
{
|
||
PostMessageW(existing, g_open_about_on_ready ? kOpenAboutSection : kActivateExistingWindow, 0, 0);
|
||
SetForegroundWindow(existing);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
WNDCLASSEXW window_class{sizeof(window_class)};
|
||
window_class.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
|
||
window_class.lpfnWndProc = WindowProc;
|
||
window_class.hInstance = instance;
|
||
window_class.hIcon = LoadIconW(instance, MAKEINTRESOURCEW(IDI_IME_ICON));
|
||
window_class.hIconSm = LoadIconW(instance, MAKEINTRESOURCEW(IDI_SETTINGS_ICON));
|
||
window_class.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||
window_class.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
|
||
window_class.lpszClassName = kWindowClass;
|
||
if (!RegisterClassExW(&window_class))
|
||
return 1;
|
||
|
||
const UINT dpi = GetDpiForSystem();
|
||
const int width = MulDiv(900, dpi, 96);
|
||
const int height = MulDiv(710, dpi, 96);
|
||
const int x = (GetSystemMetrics(SM_CXSCREEN) - width) / 2;
|
||
const int y = (GetSystemMetrics(SM_CYSCREEN) - height) / 2;
|
||
HWND hwnd = CreateWindowExW(0, kWindowClass, kWindowTitle, WS_OVERLAPPEDWINDOW, x, y, width, height, nullptr,
|
||
nullptr, instance, nullptr);
|
||
if (!hwnd)
|
||
return 1;
|
||
g_settings_hwnd = hwnd;
|
||
g_worker = std::make_unique<SerialTaskQueue>(
|
||
[hwnd] { PostMessageW(hwnd, kWorkerCompleted, 0, 0); },
|
||
[] { MessageBoxW(g_settings_hwnd, L"设置操作失败,请重试。", L"水杉输入法", MB_OK | MB_ICONWARNING); },
|
||
[] {
|
||
if (g_worker_com_initialized)
|
||
CoUninitialize();
|
||
});
|
||
g_worker->Submit([] {
|
||
g_worker_com_initialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
|
||
InitImeConfig();
|
||
return ConfigCompletion();
|
||
});
|
||
|
||
DWM_WINDOW_CORNER_PREFERENCE corner = DWMWCP_ROUND;
|
||
DWM_SYSTEMBACKDROP_TYPE backdrop = DWMSBT_NONE;
|
||
ApplySettingsChromeTheme(hwnd);
|
||
DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(corner));
|
||
DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &backdrop, sizeof(backdrop));
|
||
MARGINS margins{};
|
||
DwmExtendFrameIntoClientArea(hwnd, &margins);
|
||
|
||
// CreateWindow sends the initial WM_NCCALCSIZE before the DWM/custom-frame
|
||
// attributes above are installed. Force a second frame calculation now;
|
||
// otherwise the native caption remains until the first minimize/restore.
|
||
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||
|
||
SetTimer(hwnd, kConfigReloadTimer, 300, nullptr);
|
||
InitWebView(hwnd);
|
||
ShowWindow(hwnd, show_command == SW_HIDE ? SW_SHOWNORMAL : show_command);
|
||
UpdateWindow(hwnd);
|
||
SettingsSplash::Show(hwnd, g_settings_light);
|
||
if (g_webview_content_started)
|
||
SettingsSplash::Dismiss();
|
||
|
||
MSG message{};
|
||
while (GetMessageW(&message, nullptr, 0, 0) > 0)
|
||
{
|
||
TranslateMessage(&message);
|
||
DispatchMessageW(&message);
|
||
}
|
||
g_worker.reset(); // Drain accepted writes even if the window was destroyed externally.
|
||
CoUninitialize();
|
||
return static_cast<int>(message.wParam);
|
||
}
|