Integrate the reviewed 2.0 PRs and local fixes from 03136853. Preserve the verified subtitle behavior and model/runtime corrections. Keep the numeric Beta tag compatible with released clients while exposing the date as application build metadata. Co-authored-by: DepengWang <2818245+DepengWang@users.noreply.github.com> Co-authored-by: HKLHaoBin <we3q@qq.com> Co-authored-by: Nahida <1139500183@qq.com> Co-authored-by: TRIP <1933142963@qq.com> Co-authored-by: chyuwei <weichangyuwcy@gmail.com> Co-authored-by: leonoxo <leonoxo@gmail.com> Co-authored-by: tayir <arnoldincredible@126.com>
1069 lines
49 KiB
C++
1069 lines
49 KiB
C++
/*
|
||
* SPDX-FileCopyrightText: 2025 OpenLess Contributors
|
||
*
|
||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||
*
|
||
* fcitx5 插件 — 供 OpenLess 听写文字提交 + 快捷键监听。
|
||
*
|
||
* DBus 接口: org.fcitx.Fcitx.OpenLess1 (对象路径 /openless)
|
||
* 方法:
|
||
* CommitText(s: text) -> b — 将文字提交到当前焦点输入上下文
|
||
* 安全性:本接口在会话总线(session bus)上对同用户
|
||
* 所有进程开放,此为 fcitx5/IBus 体系的标准安全模型
|
||
* (非特权进程隔离)。
|
||
* SetHotkey(as: keys) — 设置听写触发快捷键 (Key::parse 格式)
|
||
* SetHotkeyRaw(uu: sym, states) — 直接设听写触发 sym+states (不走 parse)
|
||
* SetCustomDictationTrigger(s: keyString) — 设置自定义组合键 (Key::parse 格式)
|
||
* SetQaHotkeyRaw(uu: sym, states) — 直接设 QA 面板触发 sym+states
|
||
* SetTranslationHotkeyRaw(uu: sym, states) — 直接设翻译模式触发 sym+states
|
||
* SetLessComputerHotkeyRaw(uu: sym, states) — 直接设 Less Computer 触发 sym+states
|
||
* SetAuxDown(s: text) — 在候选词列表下方显示状态文本
|
||
* ClearAuxDown() — 清除候选词列表下方文本
|
||
* GetSelectionText() -> s — 读取当前 PRIMARY 选区文本(由 clipboard addon 维护)
|
||
* SetClipboardText(s: text) -> b — 通过 clipboard addon 写入 CLIPBOARD
|
||
* CaptureSelectionTarget(s: ticket) -> s — 捕获选区和原输入上下文
|
||
* ApplySelectionTarget(sss: ticket, source, replacement) -> b — 校验后替换
|
||
* RevertSelectionTarget(s: ticket) -> b — 校验光标前文本后撤销替换
|
||
* RekeySelectionTarget(ss: oldTicket, newTicket) -> b — 把 QA 目标交给 Core 预览
|
||
* CancelSelectionTarget(s: ticket) -> b — 释放未使用的目标
|
||
* 信号:
|
||
* DictationKeyEvent(uub: sym, states, isPress) — 听写热键按下/抬起
|
||
* LessComputerKeyEvent(uub: sym, states, isPress) — Less Computer 热键按下/抬起
|
||
* LessComputerKeyCombined(uub: sym, states, isPress) — Less Computer 组合键撤销
|
||
* QaShortcutEvent(uub: sym, states, isPress) — QA 快捷键按下/抬起
|
||
* SelectionPolishEvent(uub: sym, states, isPress) — 选区润色快捷键按下/抬起
|
||
* TranslationModifierEvent(uub: sym, states, isPress) — 翻译修饰键按下/抬起
|
||
*/
|
||
|
||
#include <algorithm>
|
||
#include <memory>
|
||
#include <sstream>
|
||
#include <iomanip>
|
||
#include <string>
|
||
#include <tuple>
|
||
#include <unordered_map>
|
||
#include <vector>
|
||
|
||
#include <fcitx-config/configuration.h>
|
||
#include <fcitx-config/iniparser.h>
|
||
#include <fcitx-config/option.h>
|
||
#include <fcitx-utils/dbus/bus.h>
|
||
#include <fcitx-utils/dbus/objectvtable.h>
|
||
#include <fcitx-utils/handlertable.h>
|
||
#include <fcitx-utils/i18n.h>
|
||
#include <fcitx-utils/key.h>
|
||
#include <fcitx-utils/log.h>
|
||
#include <fcitx-utils/utf8.h>
|
||
#include <fcitx/addonfactory.h>
|
||
#include <fcitx/addoninstance.h>
|
||
#include <fcitx/addonmanager.h>
|
||
#include <fcitx/event.h>
|
||
#include <fcitx/inputcontext.h>
|
||
#include <fcitx/inputcontextmanager.h>
|
||
#include <fcitx/inputpanel.h>
|
||
#include <fcitx/instance.h>
|
||
#include <fcitx-module/clipboard/clipboard_public.h>
|
||
#include <fcitx-module/dbus/dbus_public.h>
|
||
|
||
FCITX_DEFINE_LOG_CATEGORY(openless, "openless");
|
||
|
||
namespace fcitx {
|
||
|
||
FCITX_CONFIGURATION(OpenLessConfig,
|
||
KeyListOption triggerKey{this,
|
||
"TriggerKey",
|
||
_("Dictation trigger key"),
|
||
{},
|
||
KeyListConstrain()};
|
||
);
|
||
|
||
class OpenLess final : public AddonInstance,
|
||
public dbus::ObjectVTable<OpenLess> {
|
||
public:
|
||
OpenLess(Instance *instance)
|
||
: instance_(instance),
|
||
triggerRawSym_(0),
|
||
triggerRawStates_(0),
|
||
qaRawSym_(0),
|
||
qaRawStates_(0),
|
||
selectionPolishRawSym_(0),
|
||
selectionPolishRawStates_(0),
|
||
translationRawSym_(0),
|
||
translationRawStates_(0),
|
||
lessComputerRawSym_(0),
|
||
lessComputerRawStates_(0),
|
||
switchStyleRawSym_(0),
|
||
switchStyleRawStates_(0),
|
||
lessComputerPanelRawSym_(0),
|
||
lessComputerPanelRawStates_(0),
|
||
lessComputerQuickRawSym_(0),
|
||
lessComputerQuickRawStates_(0),
|
||
openAppRawSym_(0),
|
||
openAppRawStates_(0),
|
||
hasCustomDictationKey_(false),
|
||
dictationTriggerHeld_(false),
|
||
dictationTriggerCombined_(false),
|
||
lessComputerTriggerHeld_(false),
|
||
lessComputerTriggerCombined_(false),
|
||
savedIc_(nullptr),
|
||
selectionIc_(nullptr) {
|
||
|
||
// 1. 读取配置
|
||
reloadConfig();
|
||
|
||
// 2. 注册 DBus 接口
|
||
auto *dbusMod = instance_->addonManager().addon("dbus", true);
|
||
if (dbusMod) {
|
||
auto *bus = dbusMod->call<IDBusModule::bus>();
|
||
if (bus) {
|
||
bus->addObjectVTable(
|
||
"/openless",
|
||
"org.fcitx.Fcitx.OpenLess1",
|
||
*this);
|
||
FCITX_LOGC(openless, Info)
|
||
<< "DBus interface registered at /openless";
|
||
} else {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "Failed to get DBus bus";
|
||
}
|
||
} else {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "DBus module not available";
|
||
}
|
||
|
||
// 3. 快捷键事件监听。
|
||
// PreInputMethod 在引擎 InputMethod 阶段之前运行,
|
||
// filterAndAccept() 设 filtered+accepted → 引擎跳过 commit → 字符不泄漏。
|
||
eventHandlers_.push_back(
|
||
instance_->watchEvent(
|
||
EventType::InputContextKeyEvent,
|
||
EventWatcherPhase::PreInputMethod,
|
||
[this](Event &event) {
|
||
auto &keyEvent = static_cast<KeyEvent &>(event);
|
||
if (!keyEvent.isRelease()) {
|
||
savedIc_ = keyEvent.inputContext();
|
||
}
|
||
auto sym = static_cast<uint32_t>(keyEvent.key().sym());
|
||
auto states = static_cast<uint32_t>(keyEvent.key().states());
|
||
bool isPress = !keyEvent.isRelease();
|
||
|
||
if (lessComputerRawSym_ != 0 && sym == lessComputerRawSym_ &&
|
||
states == lessComputerRawStates_) {
|
||
lessComputerTriggerHeld_ = isPress;
|
||
if (isPress) {
|
||
lessComputerTriggerCombined_ = false;
|
||
}
|
||
lessComputerKeyEvent(sym, states, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
if (isPress && lessComputerTriggerHeld_ && !isModifierKeySym(sym) &&
|
||
!lessComputerTriggerCombined_) {
|
||
lessComputerTriggerCombined_ = true;
|
||
lessComputerKeyCombined(sym, states, true);
|
||
}
|
||
|
||
// 自定义组合键:Alt 状态下字母 sym 可能大写(A vs a),归一化比较
|
||
if (hasCustomDictationKey_ && states == static_cast<uint32_t>(customDictationKey_.states()) &&
|
||
(sym == static_cast<uint32_t>(customDictationKey_.sym()) ||
|
||
(sym >= 65 && sym <= 90 && sym + 32 == static_cast<uint32_t>(customDictationKey_.sym())) ||
|
||
(sym >= 97 && sym <= 122 && sym - 32 == static_cast<uint32_t>(customDictationKey_.sym())))) {
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "Custom dictation: sym=" << sym << " states=" << states;
|
||
dictationKeyEvent(
|
||
static_cast<uint32_t>(customDictationKey_.sym()),
|
||
static_cast<uint32_t>(customDictationKey_.states()),
|
||
isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
if ((triggerRawSym_ != 0 &&
|
||
keyEvent.key().check(Key(static_cast<KeySym>(triggerRawSym_),
|
||
static_cast<KeyStates>(triggerRawStates_)))) ||
|
||
(triggerRawSym_ == 0 && [&]() {
|
||
for (const auto &hk : triggerKeyList_) {
|
||
if (sym == static_cast<uint32_t>(hk.sym()) &&
|
||
states == static_cast<uint32_t>(hk.states()))
|
||
return true;
|
||
}
|
||
return false;
|
||
}())) {
|
||
// 修复崩溃: raw 路径(SetHotkeyRaw)匹配时若 triggerRawStates_==0,
|
||
// 原代码会无条件取 triggerKeyList_[0];而 raw 模式常伴随空 KeyList
|
||
// (见 openless.conf: TriggerKey= 为空), 对空 vector 取下标 [0]
|
||
// 是未定义行为, 直接导致 fcitx5 段错误 (Key::states 读野指针)。
|
||
// 修正: 只有 KeyList 路径匹配时才访问列表, raw 路径直接用 raw 值。
|
||
uint32_t dsym = triggerRawSym_;
|
||
uint32_t dstates = triggerRawStates_;
|
||
if (triggerRawSym_ == 0 && !triggerKeyList_.empty()) {
|
||
dsym = static_cast<uint32_t>(triggerKeyList_[0].sym());
|
||
dstates = static_cast<uint32_t>(triggerKeyList_[0].states());
|
||
}
|
||
if (dsym == 0) {
|
||
return;
|
||
}
|
||
if (!hasCustomDictationKey_ && isModifierKeySym(dsym)) {
|
||
dictationTriggerHeld_ = isPress;
|
||
if (isPress) {
|
||
dictationTriggerCombined_ = false;
|
||
}
|
||
}
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "Dictation hotkey sym=" << dsym;
|
||
dictationKeyEvent(dsym, dstates, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
if (isPress && dictationTriggerHeld_ && !isModifierKeySym(sym) &&
|
||
!dictationTriggerCombined_) {
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "Dictation hotkey combined with sym=" << sym;
|
||
dictationTriggerCombined_ = true;
|
||
dictationKeyCombined(sym, states, true);
|
||
}
|
||
if (qaRawSym_ != 0 && sym == qaRawSym_ &&
|
||
states == qaRawStates_) {
|
||
if (isPress) selectionIc_ = keyEvent.inputContext();
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "QA shortcut";
|
||
qaShortcutEvent(qaRawSym_, qaRawStates_, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
if (selectionPolishRawSym_ != 0 &&
|
||
sym == selectionPolishRawSym_ &&
|
||
states == selectionPolishRawStates_) {
|
||
if (isPress) selectionIc_ = keyEvent.inputContext();
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "Selection polish shortcut";
|
||
selectionPolishEvent(selectionPolishRawSym_,
|
||
selectionPolishRawStates_, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
bool translationMatched = false;
|
||
if (translationRawSym_ != 0 && sym == translationRawSym_ &&
|
||
states == translationRawStates_)
|
||
translationMatched = true;
|
||
if (translationRawSym_ != 0 &&
|
||
(sym == 0xffe1 || sym == 0xffe2))
|
||
translationMatched = true;
|
||
if (translationMatched) {
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "Translation modifier: sym=" << sym;
|
||
translationModifierEvent(sym, states, isPress);
|
||
}
|
||
if (switchStyleRawSym_ != 0 && sym == switchStyleRawSym_ &&
|
||
states == switchStyleRawStates_) {
|
||
switchStyleEvent(sym, states, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
if (lessComputerPanelRawSym_ != 0 && sym == lessComputerPanelRawSym_ && states == lessComputerPanelRawStates_) {
|
||
lessComputerPanelEvent(sym,states,isPress); keyEvent.filterAndAccept(); return;
|
||
}
|
||
if (lessComputerQuickRawSym_ != 0 && sym == lessComputerQuickRawSym_ && states == lessComputerQuickRawStates_) {
|
||
lessComputerQuickEvent(sym,states,isPress); keyEvent.filterAndAccept(); return;
|
||
}
|
||
if (openAppRawSym_ != 0 && sym == openAppRawSym_ &&
|
||
states == openAppRawStates_) {
|
||
openAppEvent(sym, states, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
for (const auto &[packId, packSym, packStates] : stylePackHotkeys_) {
|
||
if (packSym != 0 && sym == packSym && states == packStates) {
|
||
stylePackHotkeyEvent(sym, states, isPress);
|
||
keyEvent.filterAndAccept();
|
||
return;
|
||
}
|
||
}
|
||
}));
|
||
|
||
// 4. 监听 InputContext 销毁事件,自动清空 savedIc_ 避免野指针
|
||
eventHandlers_.push_back(
|
||
instance_->watchEvent(
|
||
EventType::InputContextDestroyed,
|
||
EventWatcherPhase::Default,
|
||
[this](Event &event) {
|
||
auto &icEvent = static_cast<InputContextEvent &>(event);
|
||
for (auto it = contextTargets_.begin(); it != contextTargets_.end();) {
|
||
if (it->second == icEvent.inputContext()) it = contextTargets_.erase(it);
|
||
else ++it;
|
||
}
|
||
if (icEvent.inputContext() == savedIc_) {
|
||
savedIc_ = nullptr;
|
||
}
|
||
if (icEvent.inputContext() == selectionIc_) {
|
||
selectionIc_ = nullptr;
|
||
}
|
||
for (auto it = selectionTargets_.begin();
|
||
it != selectionTargets_.end();) {
|
||
if (it->second.inputContext == icEvent.inputContext()) {
|
||
it = selectionTargets_.erase(it);
|
||
} else {
|
||
++it;
|
||
}
|
||
}
|
||
for (auto it = dictationTargets_.begin(); it != dictationTargets_.end();) {
|
||
if (it->second == icEvent.inputContext()) {
|
||
it = dictationTargets_.erase(it);
|
||
} else {
|
||
++it;
|
||
}
|
||
}
|
||
}));
|
||
|
||
// 5. 监听焦点切换:用户切窗口时把上次 auxDown 自动补到新 IC,
|
||
// 确保听写状态提示跟随焦点移动。
|
||
eventHandlers_.push_back(
|
||
instance_->watchEvent(
|
||
EventType::InputContextFocusIn,
|
||
EventWatcherPhase::Default,
|
||
[this](Event &event) {
|
||
if (lastAuxText_.empty()) return;
|
||
auto &icEvent = static_cast<InputContextEvent &>(event);
|
||
auto *ic = icEvent.inputContext();
|
||
if (!ic) return;
|
||
instance_->flushUI();
|
||
ic->inputPanel().setAuxDown(Text(lastAuxText_));
|
||
ic->updatePreedit();
|
||
ic->updateUserInterface(UserInterfaceComponent::InputPanel, true);
|
||
instance_->flushUI();
|
||
}));
|
||
|
||
// 6. PostInputMethod:恢复 auxDown(fcitx5 内联模式/方向键后可能清掉)
|
||
eventHandlers_.push_back(
|
||
instance_->watchEvent(
|
||
EventType::InputContextKeyEvent,
|
||
EventWatcherPhase::PostInputMethod,
|
||
[this](Event &event) {
|
||
if (lastAuxText_.empty()) return;
|
||
auto &keyEvent = static_cast<KeyEvent &>(event);
|
||
auto *ic = keyEvent.inputContext();
|
||
if (!ic) return;
|
||
ic->inputPanel().setAuxDown(Text(lastAuxText_));
|
||
ic->updateUserInterface(UserInterfaceComponent::InputPanel, true);
|
||
}));
|
||
|
||
FCITX_LOGC(openless, Info) << "OpenLess plugin loaded";
|
||
}
|
||
|
||
~OpenLess() = default;
|
||
|
||
// ---- DBus 方法 ----
|
||
// 返回 bool,让调用方区分“无焦点输入上下文”的安全失败和实际提交成功。
|
||
|
||
bool commitText(const std::string &text) {
|
||
// 优先使用快捷键按下时保存的输入上下文(savedIc_),
|
||
// 此时用户在目标 app 中,此后胶囊窗口抢焦点不影响提交。
|
||
// 若 savedIc_ 为空则兜底用 foreachFocused。
|
||
auto *ic = savedIc_;
|
||
if (!ic) {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "CommitText: savedIc_ is null, trying foreachFocused";
|
||
auto &mgr = instance_->inputContextManager();
|
||
mgr.foreachFocused([&](InputContext *focusedIc) {
|
||
ic = focusedIc;
|
||
return false;
|
||
});
|
||
}
|
||
if (!ic) {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "CommitText: no input context available";
|
||
// A DBus call must not bring down the fcitx5 host when the target
|
||
// application has no focused input context (for example during
|
||
// startup or in a headless session). The Rust adapter observes
|
||
// the successful method return and can use its own capability or
|
||
// clipboard fallback policy; fcitx5 remains alive either way.
|
||
return false;
|
||
}
|
||
FCITX_LOGC(openless, Debug) << "CommitText: " << text;
|
||
ic->commitString(text);
|
||
return true;
|
||
}
|
||
|
||
// Independently versioned read-only bridge. A target UUID never aliases a
|
||
// newly created input context, and includeText=false never reads its text.
|
||
std::string contextSnapshot(const std::string &expected, bool includeText) {
|
||
InputContext *ic = nullptr;
|
||
bool selectionTicket = false;
|
||
if (expected.empty()) {
|
||
instance_->inputContextManager().foreachFocused([&ic](InputContext *candidate) {
|
||
ic = candidate; return false;
|
||
});
|
||
} else {
|
||
auto found = contextTargets_.find(expected);
|
||
if (found != contextTargets_.end()) ic = found->second;
|
||
auto selected = selectionTargets_.find(expected);
|
||
if (selected != selectionTargets_.end()) { ic=selected->second.inputContext; selectionTicket=true; }
|
||
}
|
||
if (!ic || !ic->hasFocus()) return "{}";
|
||
std::ostringstream id;
|
||
for (auto byte : ic->uuid()) id << std::hex << std::setw(2) << std::setfill('0') << static_cast<unsigned>(byte);
|
||
const auto target = selectionTicket ? expected : id.str();
|
||
if (!expected.empty() && expected != target) return "{}";
|
||
if (!selectionTicket) contextTargets_[target] = ic;
|
||
const bool sensitive = ic->capabilityFlags().test(CapabilityFlag::Password);
|
||
auto quote = [](const std::string &value) {
|
||
std::ostringstream out; out << '"';
|
||
for (unsigned char c : value) {
|
||
if (c == '"' || c == '\\') out << '\\' << c;
|
||
else if (c < 0x20) out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<unsigned>(c);
|
||
else out << c;
|
||
}
|
||
out << '"'; return out.str();
|
||
};
|
||
std::ostringstream out;
|
||
out << "{\"version\":1,\"target\":" << quote(target)
|
||
<< ",\"application\":" << quote(ic->program())
|
||
<< ",\"sensitive\":" << (sensitive ? "true" : "false");
|
||
if (includeText && !sensitive && ic->surroundingText().isValid()) {
|
||
const auto &text = ic->surroundingText();
|
||
// Reject oversized documents rather than returning a moving window
|
||
// that could be mistaken for a user edit by the observer.
|
||
if (text.text().size() <= 65536)
|
||
out << ",\"text\":" << quote(text.text()) << ",\"cursor\":" << text.cursor();
|
||
}
|
||
out << '}'; return out.str();
|
||
}
|
||
|
||
std::string captureSelectionTarget(const std::string &ticket) {
|
||
selectionIc_ = nullptr;
|
||
instance_->inputContextManager().foreachFocused([this](InputContext *ic) {
|
||
selectionIc_ = ic; return false;
|
||
});
|
||
if (ticket.empty() || !selectionIc_ || selectionIc_->capabilityFlags().test(CapabilityFlag::Password)) {
|
||
return std::string();
|
||
}
|
||
std::string source;
|
||
const auto &surrounding = selectionIc_->surroundingText();
|
||
if (surrounding.isValid()) {
|
||
source = surrounding.selectedText();
|
||
}
|
||
if (source.empty()) {
|
||
source = getSelectionText();
|
||
}
|
||
if (source.empty()) {
|
||
return std::string();
|
||
}
|
||
selectionTargets_[ticket] = {
|
||
selectionIc_, source, std::string(), surrounding.text(),
|
||
surrounding.cursor(), surrounding.anchor(), surrounding.isValid()};
|
||
return source;
|
||
}
|
||
|
||
bool captureDictationTarget(const std::string &ticket) {
|
||
savedIc_ = nullptr;
|
||
instance_->inputContextManager().foreachFocused([this](InputContext *ic) {
|
||
savedIc_ = ic; return false;
|
||
});
|
||
if (ticket.empty() || !savedIc_) return false;
|
||
// A session keeps its own native target even when later key events
|
||
// update savedIc_. Destruction invalidates the ticket instead of
|
||
// redirecting the remaining transcript to a different application.
|
||
return dictationTargets_.emplace(ticket, savedIc_).second;
|
||
}
|
||
|
||
bool commitDictationTarget(const std::string &ticket, const std::string &text) {
|
||
auto found = dictationTargets_.find(ticket);
|
||
if (found == dictationTargets_.end() || !found->second->hasFocus()) return false;
|
||
found->second->commitString(text);
|
||
return true;
|
||
}
|
||
|
||
bool cancelDictationTarget(const std::string &ticket) {
|
||
return dictationTargets_.erase(ticket) > 0;
|
||
}
|
||
|
||
bool applySelectionTarget(const std::string &ticket,
|
||
const std::string &source,
|
||
const std::string &replacement) {
|
||
// The ticket is the Core session generation. Never fall back to the
|
||
// current focus here: a preview may have focused the OpenLess window,
|
||
// and writing there would corrupt a different application.
|
||
auto found = selectionTargets_.find(ticket);
|
||
if (found == selectionTargets_.end() || source != found->second.source ||
|
||
replacement.empty()) {
|
||
return false;
|
||
}
|
||
auto *ic = found->second.inputContext;
|
||
if (!ic->hasFocus()) return false;
|
||
const auto &surrounding = ic->surroundingText();
|
||
const auto &captured = found->second;
|
||
// PRIMARY can outlive the selection, and the same selected string may
|
||
// occur at several offsets. Only the original IC's complete surrounding
|
||
// snapshot proves that this exact range is still the intended target.
|
||
// Without surrounding-text support, preview/read remains possible but
|
||
// destructive replacement must fail safely.
|
||
if (!captured.surroundingValid || !surrounding.isValid() ||
|
||
captured.surroundingText != surrounding.text() ||
|
||
captured.cursor != surrounding.cursor() ||
|
||
captured.anchor != surrounding.anchor() ||
|
||
surrounding.selectedText() != source) {
|
||
return false;
|
||
}
|
||
ic->commitString(replacement);
|
||
found->second.replacement = replacement;
|
||
return true;
|
||
}
|
||
|
||
bool revertSelectionTarget(const std::string &ticket) {
|
||
auto found = selectionTargets_.find(ticket);
|
||
if (found == selectionTargets_.end() || found->second.replacement.empty()) {
|
||
return false;
|
||
}
|
||
auto *ic = found->second.inputContext;
|
||
if (!ic->hasFocus()) return false;
|
||
const auto &replacement = found->second.replacement;
|
||
const auto &surrounding = ic->surroundingText();
|
||
if (!surrounding.isValid()) {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "RevertSelectionTarget: surrounding text is unavailable";
|
||
return false;
|
||
}
|
||
const auto replacementChars = utf8::lengthValidated(replacement);
|
||
const auto textChars = utf8::lengthValidated(surrounding.text());
|
||
if (replacementChars == utf8::INVALID_LENGTH ||
|
||
textChars == utf8::INVALID_LENGTH ||
|
||
surrounding.cursor() > textChars ||
|
||
surrounding.cursor() < replacementChars) {
|
||
return false;
|
||
}
|
||
const auto &captured = found->second;
|
||
if (surrounding.cursor() != std::min(captured.cursor, captured.anchor) + replacementChars ||
|
||
surrounding.anchor() != surrounding.cursor()) {
|
||
return false;
|
||
}
|
||
auto end = utf8::nextNChar(
|
||
surrounding.text().begin(), surrounding.cursor());
|
||
auto begin = utf8::nextNChar(
|
||
surrounding.text().begin(), surrounding.cursor() - replacementChars);
|
||
if (std::string(begin, end) != replacement) {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "RevertSelectionTarget: text changed after replacement";
|
||
return false;
|
||
}
|
||
ic->deleteSurroundingText(-static_cast<int>(replacementChars),
|
||
static_cast<unsigned int>(replacementChars));
|
||
ic->commitString(found->second.source);
|
||
selectionTargets_.erase(found);
|
||
return true;
|
||
}
|
||
|
||
bool cancelSelectionTarget(const std::string &ticket) {
|
||
return selectionTargets_.erase(ticket) > 0;
|
||
}
|
||
|
||
bool rekeySelectionTarget(const std::string &oldTicket,
|
||
const std::string &newTicket) {
|
||
auto found = selectionTargets_.find(oldTicket);
|
||
if (found == selectionTargets_.end() || newTicket.empty()) {
|
||
return false;
|
||
}
|
||
auto target = std::move(found->second);
|
||
selectionTargets_.erase(found);
|
||
selectionTargets_[newTicket] = std::move(target);
|
||
return true;
|
||
}
|
||
|
||
void setAuxDown(const std::string &text) {
|
||
// 优先用当前焦点 IC(输入面板只在焦点 IC 上渲染),
|
||
// 降级到 savedIc_(快捷键按下时捕获的 IC,可能已失焦但指针仍有效)。
|
||
InputContext *ic = nullptr;
|
||
auto &mgr = instance_->inputContextManager();
|
||
mgr.foreachFocused([&](InputContext *focusedIc) {
|
||
ic = focusedIc;
|
||
return false;
|
||
});
|
||
if (!ic) {
|
||
ic = savedIc_;
|
||
}
|
||
if (!ic) {
|
||
FCITX_LOGC(openless, Warn) << "SetStatusCandidates: no IC (focused=null, saved=null)";
|
||
return;
|
||
}
|
||
FCITX_LOGC(openless, Info) << "SetStatusCandidates: " << text
|
||
<< " ic=" << ic << " focused=" << (ic != savedIc_ ? "current" : "saved");
|
||
lastAuxText_ = text;
|
||
// 先把事件队列里挂起的旧 UI 更新处理掉(例如前一个按键触发的面板重置),
|
||
// 再设置 auxDown,确保不会被待处理事件覆盖。
|
||
instance_->flushUI();
|
||
ic->inputPanel().setAuxDown(Text(text));
|
||
ic->updatePreedit();
|
||
ic->updateUserInterface(UserInterfaceComponent::InputPanel, true);
|
||
instance_->flushUI();
|
||
}
|
||
|
||
void clearAuxDown() {
|
||
// 无论是否有可用 IC,都要清掉缓存的状态文字,否则下一次 FocusIn
|
||
// 会把旧状态(如"已插入")重放到新聚焦的窗口。
|
||
lastAuxText_.clear();
|
||
InputContext *ic = nullptr;
|
||
auto &mgr = instance_->inputContextManager();
|
||
mgr.foreachFocused([&](InputContext *focusedIc) {
|
||
ic = focusedIc;
|
||
return false;
|
||
});
|
||
if (!ic) {
|
||
ic = savedIc_;
|
||
}
|
||
if (!ic) return;
|
||
FCITX_LOGC(openless, Info) << "ClearStatusCandidates";
|
||
ic->inputPanel().setAuxDown(Text());
|
||
ic->updatePreedit();
|
||
ic->updateUserInterface(UserInterfaceComponent::InputPanel, true);
|
||
instance_->flushUI();
|
||
}
|
||
|
||
void setHotkey(const std::vector<std::string> &keys) {
|
||
// 切换预设修饰键时清空自定义组合键,避免双发
|
||
hasCustomDictationKey_ = false;
|
||
resetDictationTriggerState();
|
||
KeyList keyList;
|
||
for (const auto &s : keys) {
|
||
Key key(s);
|
||
if (key.isValid()) {
|
||
keyList.push_back(key);
|
||
} else {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "SetHotkey: invalid key '" << s << "'";
|
||
}
|
||
}
|
||
config_.triggerKey.setValue(keyList);
|
||
// KeyList 路径激活时清空 raw 路径,避免优先级冲突
|
||
triggerRawSym_ = 0;
|
||
triggerRawStates_ = 0;
|
||
safeSaveAsIni(config_, configFile());
|
||
// 同时清除磁盘上残留的 TriggerRawSym/TriggerRawStates(旧 raw 模式的持久化值),
|
||
// 防止下次 fcitx5 重启 reloadConfig 重新加载旧 raw 热键覆盖新配置。
|
||
{
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath("TriggerRawSym", "0");
|
||
raw.setValueByPath("TriggerRawStates", "0");
|
||
safeSaveAsIni(raw, configFile());
|
||
}
|
||
rebuildTriggerKeys();
|
||
}
|
||
|
||
void setHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
// 切换预设修饰键时清空自定义组合键,避免双发
|
||
hasCustomDictationKey_ = false;
|
||
resetDictationTriggerState();
|
||
triggerRawSym_ = sym;
|
||
triggerRawStates_ = states;
|
||
// 同时尝试维护 KeyList(如果 sym 可转为有效 key)
|
||
Key key(static_cast<KeySym>(sym),
|
||
static_cast<KeyStates>(states));
|
||
if (key.isValid()) {
|
||
KeyList keys = {key};
|
||
config_.triggerKey.setValue(keys);
|
||
} else {
|
||
// 修饰键无法用 KeyList 表达,清空 KeyList 避免误匹配
|
||
config_.triggerKey.setValue(KeyList{});
|
||
}
|
||
// 合并写入 config 和 raw sym/states
|
||
RawConfig raw;
|
||
raw.setValueByPath("TriggerRawSym", std::to_string(sym));
|
||
raw.setValueByPath("TriggerRawStates", std::to_string(states));
|
||
raw.setValueByPath("CustomDictationKey", "");
|
||
config_.save(raw);
|
||
safeSaveAsIni(raw, configFile());
|
||
rebuildTriggerKeys();
|
||
}
|
||
|
||
void setCustomDictationTrigger(const std::string &keyString) {
|
||
Key key(keyString);
|
||
if (!key.isValid()) {
|
||
FCITX_LOGC(openless, Warn)
|
||
<< "SetCustomDictationTrigger: invalid key '" << keyString << "'";
|
||
hasCustomDictationKey_ = false;
|
||
resetDictationTriggerState();
|
||
return;
|
||
}
|
||
customDictationKey_ = key;
|
||
hasCustomDictationKey_ = true;
|
||
resetDictationTriggerState();
|
||
// 有自定义键时清空已有 raw+keylist 路径,避免双发
|
||
triggerRawSym_ = 0;
|
||
triggerRawStates_ = 0;
|
||
config_.triggerKey.setValue(KeyList{});
|
||
// 同时持久化清空 TriggerRawSym/TriggerRawStates,防止 fcitx5 重启后从 INI 加载旧值
|
||
{
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
config_.save(raw);
|
||
raw.setValueByPath("TriggerRawSym", "0");
|
||
raw.setValueByPath("TriggerRawStates", "0");
|
||
// Persist the actual custom binding, not only removal of the old
|
||
// raw binding, so an independent fcitx5 restart retains the key.
|
||
raw.setValueByPath("CustomDictationKey", keyString);
|
||
safeSaveAsIni(raw, configFile());
|
||
}
|
||
FCITX_LOGC(openless, Info)
|
||
<< "SetCustomDictationTrigger: '" << keyString << "'"
|
||
<< " sym=" << static_cast<uint32_t>(key.sym())
|
||
<< " states=" << static_cast<uint32_t>(key.states());
|
||
}
|
||
|
||
void setQaHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
qaRawSym_ = sym;
|
||
qaRawStates_ = states;
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath("QaRawSym", std::to_string(sym));
|
||
raw.setValueByPath("QaRawStates", std::to_string(states));
|
||
safeSaveAsIni(raw, configFile());
|
||
FCITX_LOGC(openless, Info)
|
||
<< "SetQaHotkeyRaw: sym=" << sym << " states=" << states;
|
||
}
|
||
|
||
void setSelectionPolishHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
selectionPolishRawSym_ = sym;
|
||
selectionPolishRawStates_ = states;
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath("SelectionPolishRawSym", std::to_string(sym));
|
||
raw.setValueByPath("SelectionPolishRawStates", std::to_string(states));
|
||
safeSaveAsIni(raw, configFile());
|
||
FCITX_LOGC(openless, Info)
|
||
<< "SetSelectionPolishHotkeyRaw: sym=" << sym << " states=" << states;
|
||
}
|
||
|
||
void setTranslationHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
translationRawSym_ = sym;
|
||
translationRawStates_ = states;
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath("TranslationRawSym", std::to_string(sym));
|
||
raw.setValueByPath("TranslationRawStates", std::to_string(states));
|
||
safeSaveAsIni(raw, configFile());
|
||
FCITX_LOGC(openless, Info)
|
||
<< "SetTranslationHotkeyRaw: sym=" << sym << " states=" << states;
|
||
}
|
||
|
||
void setLessComputerHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
lessComputerRawSym_ = sym;
|
||
lessComputerRawStates_ = states;
|
||
lessComputerTriggerHeld_ = false;
|
||
lessComputerTriggerCombined_ = false;
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath("LessComputerRawSym", std::to_string(sym));
|
||
raw.setValueByPath("LessComputerRawStates", std::to_string(states));
|
||
safeSaveAsIni(raw, configFile());
|
||
}
|
||
|
||
void setSwitchStyleHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
switchStyleRawSym_ = sym;
|
||
switchStyleRawStates_ = states;
|
||
persistRawHotkey("SwitchStyle", sym, states);
|
||
}
|
||
|
||
void setLessComputerPanelHotkeyRaw(uint32_t sym,uint32_t states) {
|
||
lessComputerPanelRawSym_=sym; lessComputerPanelRawStates_=states; persistRawHotkey("LessComputerPanel",sym,states);
|
||
}
|
||
|
||
void setLessComputerQuickHotkeyRaw(uint32_t sym,uint32_t states) {
|
||
lessComputerQuickRawSym_=sym; lessComputerQuickRawStates_=states; persistRawHotkey("LessComputerQuick",sym,states);
|
||
}
|
||
|
||
void setOpenAppHotkeyRaw(uint32_t sym, uint32_t states) {
|
||
openAppRawSym_ = sym;
|
||
openAppRawStates_ = states;
|
||
persistRawHotkey("OpenApp", sym, states);
|
||
}
|
||
|
||
void setStylePackHotkeys(
|
||
const std::vector<dbus::DBusStruct<std::string, uint32_t, uint32_t>> &bindings) {
|
||
stylePackHotkeys_.clear();
|
||
stylePackHotkeys_.reserve(bindings.size());
|
||
for (const auto &binding : bindings) {
|
||
stylePackHotkeys_.push_back(binding.data());
|
||
}
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath("StylePackHotkeyCount", std::to_string(bindings.size()));
|
||
for (size_t index = 0; index < stylePackHotkeys_.size(); ++index) {
|
||
const auto prefix = "StylePackHotkey" + std::to_string(index);
|
||
raw.setValueByPath(prefix + "Id", std::get<0>(stylePackHotkeys_[index]));
|
||
raw.setValueByPath(prefix + "Sym", std::to_string(std::get<1>(stylePackHotkeys_[index])));
|
||
raw.setValueByPath(prefix + "States", std::to_string(std::get<2>(stylePackHotkeys_[index])));
|
||
}
|
||
safeSaveAsIni(raw, configFile());
|
||
}
|
||
|
||
/// 读取当前 PRIMARY 选区文本。空字符串表示无选区或 clipboard addon 不可用。
|
||
std::string getSelectionText() {
|
||
auto *clipboard = instance_->addonManager().addon("clipboard");
|
||
if (!clipboard) {
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "GetSelectionText: clipboard addon not loaded";
|
||
return std::string();
|
||
}
|
||
// primary() 签名接收 const InputContext*,clipboard 模块实现中未使用该参数
|
||
// (读的是全局 primary_ 缓存),这里传 nullptr 即可。
|
||
std::string text = clipboard->call<IClipboard::primary>(nullptr);
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "GetSelectionText: " << text.size() << " chars";
|
||
return text;
|
||
}
|
||
|
||
bool setClipboardText(const std::string &text) {
|
||
auto *clipboard = instance_->addonManager().addon("clipboard");
|
||
if (!clipboard) {
|
||
FCITX_LOGC(openless, Debug)
|
||
<< "SetClipboardText: clipboard addon not loaded";
|
||
return false;
|
||
}
|
||
clipboard->call<IClipboard::setClipboard>("openless", text);
|
||
return true;
|
||
}
|
||
|
||
FCITX_OBJECT_VTABLE_METHOD(commitText, "CommitText", "s", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(contextSnapshot, "ContextSnapshot", "sb", "s");
|
||
FCITX_OBJECT_VTABLE_METHOD(captureDictationTarget, "CaptureDictationTarget", "s", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(commitDictationTarget, "CommitDictationTarget", "ss", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(cancelDictationTarget, "CancelDictationTarget", "s", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(captureSelectionTarget, "CaptureSelectionTarget", "s", "s");
|
||
FCITX_OBJECT_VTABLE_METHOD(applySelectionTarget, "ApplySelectionTarget", "sss", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(revertSelectionTarget, "RevertSelectionTarget", "s", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(rekeySelectionTarget, "RekeySelectionTarget", "ss", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(cancelSelectionTarget, "CancelSelectionTarget", "s", "b");
|
||
FCITX_OBJECT_VTABLE_METHOD(setAuxDown, "SetAuxDown", "s", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(clearAuxDown, "ClearAuxDown", "", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setHotkey, "SetHotkey", "as", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setHotkeyRaw, "SetHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setCustomDictationTrigger, "SetCustomDictationTrigger", "s", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setQaHotkeyRaw, "SetQaHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setSelectionPolishHotkeyRaw, "SetSelectionPolishHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setTranslationHotkeyRaw, "SetTranslationHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setLessComputerHotkeyRaw, "SetLessComputerHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setSwitchStyleHotkeyRaw, "SetSwitchStyleHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setLessComputerPanelHotkeyRaw,"SetLessComputerPanelHotkeyRaw","uu","");
|
||
FCITX_OBJECT_VTABLE_METHOD(setLessComputerQuickHotkeyRaw,"SetLessComputerQuickHotkeyRaw","uu","");
|
||
FCITX_OBJECT_VTABLE_METHOD(setOpenAppHotkeyRaw, "SetOpenAppHotkeyRaw", "uu", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(setStylePackHotkeys, "SetStylePackHotkeys", "a(suu)", "");
|
||
FCITX_OBJECT_VTABLE_METHOD(getSelectionText, "GetSelectionText", "", "s");
|
||
FCITX_OBJECT_VTABLE_METHOD(setClipboardText, "SetClipboardText", "s", "b");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(dictationKeyEvent, "DictationKeyEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(dictationKeyCombined, "DictationKeyCombined", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(lessComputerKeyEvent, "LessComputerKeyEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(lessComputerKeyCombined, "LessComputerKeyCombined", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(qaShortcutEvent, "QaShortcutEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(selectionPolishEvent, "SelectionPolishEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(translationModifierEvent, "TranslationModifierEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(switchStyleEvent, "SwitchStyleEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(lessComputerPanelEvent,"LessComputerPanelEvent","uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(lessComputerQuickEvent,"LessComputerQuickEvent","uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(openAppEvent, "OpenAppEvent", "uub");
|
||
FCITX_OBJECT_VTABLE_SIGNAL(stylePackHotkeyEvent, "StylePackHotkeyEvent", "uub");
|
||
|
||
Instance *instance() { return instance_; }
|
||
|
||
void reloadConfig() override {
|
||
resetDictationTriggerState();
|
||
readAsIni(config_, configFile());
|
||
// 加载原始 sym/states(由 SetHotkeyRaw / SetQaHotkeyRaw / SetTranslationHotkeyRaw 写入的持久化键值)
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
{
|
||
auto *v = raw.valueByPath("TriggerRawSym");
|
||
triggerRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("TriggerRawStates");
|
||
triggerRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("QaRawSym");
|
||
qaRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("QaRawStates");
|
||
qaRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("SelectionPolishRawSym");
|
||
selectionPolishRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("SelectionPolishRawStates");
|
||
selectionPolishRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("TranslationRawSym");
|
||
translationRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("TranslationRawStates");
|
||
translationRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("LessComputerRawSym");
|
||
lessComputerRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
{
|
||
auto *v = raw.valueByPath("LessComputerRawStates");
|
||
lessComputerRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
|
||
}
|
||
loadRawHotkey(raw, "SwitchStyle", switchStyleRawSym_, switchStyleRawStates_);
|
||
loadRawHotkey(raw,"LessComputerPanel",lessComputerPanelRawSym_,lessComputerPanelRawStates_);
|
||
loadRawHotkey(raw,"LessComputerQuick",lessComputerQuickRawSym_,lessComputerQuickRawStates_);
|
||
loadRawHotkey(raw, "OpenApp", openAppRawSym_, openAppRawStates_);
|
||
stylePackHotkeys_.clear();
|
||
if (auto *countValue = raw.valueByPath("StylePackHotkeyCount")) {
|
||
const auto count = std::min<size_t>(
|
||
std::stoul(*countValue, nullptr, 0), 128);
|
||
for (size_t index = 0; index < count; ++index) {
|
||
const auto prefix = "StylePackHotkey" + std::to_string(index);
|
||
auto *id = raw.valueByPath(prefix + "Id");
|
||
auto *sym = raw.valueByPath(prefix + "Sym");
|
||
auto *states = raw.valueByPath(prefix + "States");
|
||
if (id && sym && states && !id->empty()) {
|
||
stylePackHotkeys_.emplace_back(
|
||
*id, std::stoul(*sym, nullptr, 0),
|
||
std::stoul(*states, nullptr, 0));
|
||
}
|
||
}
|
||
}
|
||
lessComputerTriggerHeld_ = false;
|
||
lessComputerTriggerCombined_ = false;
|
||
rebuildTriggerKeys();
|
||
hasCustomDictationKey_ = false;
|
||
if (auto *value = raw.valueByPath("CustomDictationKey"); value && !value->empty()) {
|
||
Key key(*value);
|
||
if (key.isValid()) {
|
||
customDictationKey_ = key;
|
||
hasCustomDictationKey_ = true;
|
||
triggerRawSym_ = 0;
|
||
triggerKeyList_.clear();
|
||
}
|
||
}
|
||
}
|
||
|
||
const Configuration *getConfig() const override {
|
||
return &config_;
|
||
}
|
||
|
||
void setConfig(const RawConfig &rawConfig) override {
|
||
config_.load(rawConfig, true);
|
||
safeSaveAsIni(config_, configFile());
|
||
rebuildTriggerKeys();
|
||
}
|
||
|
||
private:
|
||
// The native-boundary contract fixture supplies real in-process IC handles
|
||
// without synthesizing DBus signals or touching the user's input devices.
|
||
friend struct OpenLessInputTargetContract;
|
||
struct SelectionTarget {
|
||
InputContext *inputContext;
|
||
std::string source;
|
||
std::string replacement;
|
||
// Keep an owned value snapshot, not the live fcitx object: Ubuntu 22.04's
|
||
// Fcitx 5.0.14 SurroundingText is neither copyable nor movable. Plain
|
||
// values also keep the captured range unchanged as the client updates
|
||
// its live context or Core transfers this ticket from QA to a preview.
|
||
std::string surroundingText;
|
||
// Fcitx cursor/anchor offsets count Unicode characters, not UTF-8 bytes;
|
||
// preserve those units for both stale-range checks and undo placement.
|
||
unsigned int cursor;
|
||
unsigned int anchor;
|
||
bool surroundingValid;
|
||
};
|
||
|
||
static constexpr const char *configFile() {
|
||
return "conf/openless.conf";
|
||
}
|
||
|
||
static bool isModifierKeySym(uint32_t sym) {
|
||
// X11 modifier keysyms. CapsLock is included to match the desktop hook's
|
||
// treatment of lock keys: pressing it alongside a trigger must not abort
|
||
// dictation as if it were a printable companion key.
|
||
return sym >= 0xffe1 && sym <= 0xffee;
|
||
}
|
||
|
||
void resetDictationTriggerState() {
|
||
dictationTriggerHeld_ = false;
|
||
dictationTriggerCombined_ = false;
|
||
}
|
||
|
||
void rebuildTriggerKeys() {
|
||
triggerKeyList_ = config_.triggerKey.value();
|
||
}
|
||
|
||
void persistRawHotkey(const std::string &name, uint32_t sym,
|
||
uint32_t states) {
|
||
RawConfig raw;
|
||
readAsIni(raw, configFile());
|
||
raw.setValueByPath(name + "RawSym", std::to_string(sym));
|
||
raw.setValueByPath(name + "RawStates", std::to_string(states));
|
||
safeSaveAsIni(raw, configFile());
|
||
}
|
||
|
||
static void loadRawHotkey(RawConfig &raw, const std::string &name,
|
||
uint32_t &sym, uint32_t &states) {
|
||
auto *symValue = raw.valueByPath(name + "RawSym");
|
||
auto *statesValue = raw.valueByPath(name + "RawStates");
|
||
sym = symValue ? std::stoul(*symValue, nullptr, 0) : 0;
|
||
states = statesValue ? std::stoul(*statesValue, nullptr, 0) : 0;
|
||
}
|
||
|
||
Instance *instance_;
|
||
OpenLessConfig config_;
|
||
KeyList triggerKeyList_;
|
||
uint32_t triggerRawSym_;
|
||
uint32_t triggerRawStates_;
|
||
uint32_t qaRawSym_;
|
||
uint32_t qaRawStates_;
|
||
uint32_t selectionPolishRawSym_;
|
||
uint32_t selectionPolishRawStates_;
|
||
uint32_t translationRawSym_;
|
||
uint32_t translationRawStates_;
|
||
uint32_t lessComputerRawSym_;
|
||
uint32_t lessComputerRawStates_;
|
||
uint32_t switchStyleRawSym_;
|
||
uint32_t switchStyleRawStates_;
|
||
uint32_t lessComputerPanelRawSym_;
|
||
uint32_t lessComputerPanelRawStates_;
|
||
uint32_t lessComputerQuickRawSym_;
|
||
uint32_t lessComputerQuickRawStates_;
|
||
uint32_t openAppRawSym_;
|
||
uint32_t openAppRawStates_;
|
||
std::vector<std::tuple<std::string, uint32_t, uint32_t>> stylePackHotkeys_;
|
||
Key customDictationKey_;
|
||
bool hasCustomDictationKey_;
|
||
bool dictationTriggerHeld_;
|
||
bool dictationTriggerCombined_;
|
||
bool lessComputerTriggerHeld_;
|
||
bool lessComputerTriggerCombined_;
|
||
/// 快捷键按下时保存的输入上下文指针,用于 commitText 在失焦后仍能提交文字。
|
||
/// 事件处理线程和 DBus 处理线程都是 fcitx5 主事件循环,无竞态。
|
||
/// 通过 InputContextDestroyed 事件监听 IC 销毁时自动清空指针。
|
||
InputContext *savedIc_;
|
||
std::unordered_map<std::string, InputContext *> contextTargets_;
|
||
/// QA/Selection 快捷键按下时的原输入上下文。该指针只能由 fcitx5 主事件循环
|
||
/// 访问,并在 InputContextDestroyed 中与所有关联 ticket 一起失效。
|
||
InputContext *selectionIc_;
|
||
/// Core session UUID -> Host 原生目标。map 只保存 effect 所需的句柄和回滚文本;
|
||
/// Preview/Apply/Completed/Cancelled 状态仍由 Core 独占。
|
||
std::unordered_map<std::string, SelectionTarget> selectionTargets_;
|
||
std::unordered_map<std::string, InputContext *> dictationTargets_;
|
||
/// 上一次 SetAuxDown 的文本;焦点切换时用于自动补到新 IC。
|
||
std::string lastAuxText_;
|
||
std::vector<std::unique_ptr<HandlerTableEntry<EventHandler>>>
|
||
eventHandlers_;
|
||
};
|
||
|
||
class OpenLessFactory : public AddonFactory {
|
||
public:
|
||
AddonInstance *create(AddonManager *manager) override {
|
||
return new OpenLess(manager->instance());
|
||
}
|
||
};
|
||
|
||
} // namespace fcitx
|
||
|
||
FCITX_ADDON_FACTORY(fcitx::OpenLessFactory);
|