diff --git a/src/lotus-config.h b/src/lotus-config.h index 6c2177a7..cf74b784 100644 --- a/src/lotus-config.h +++ b/src/lotus-config.h @@ -233,20 +233,29 @@ namespace fcitx { Option capitalizeMacro{this, "CapitalizeMacro", _("Capitalize Macro"), true}; OptionWithAnnotation macroSkipTriggerModifier{ this, "MacroSkipTriggerModifier", _("Modifier to Skip Macro for Next Word"), MacroSkipTriggerModifier::Disabled}; - Option autoCapitalizeAfterPunctuation{this, "AutoCapitalizeAfterPunctuation", _("Auto capitalize after sentence-ending punctuation (. ! ? Enter)"), false}; - Option doubleSpaceToPeriod{this, "DoubleSpaceToPeriod", _("Double Space to Period"), false}; - Option doubleHyphenToEmDash{this, "DoubleHyphenToEmDash", _("Double Hyphen to Em-Dash (--)"), false}; - Option autoNonVnRestore{this, "AutoNonVnRestore", _("Auto Restore Invalid Words"), true}; - Option modernStyle{this, "ModernStyle", _("Use oà, uý (Instead Of òa, úy)"), true}; - Option freeMarking{this, "FreeMarking", _("Allow Type With More Freedom"), true}; - Option ddFreeStyle{this, "DdFreeStyle", _("Allow dd To Produce đ When Auto Restore Invalid Words Is On"), true}; - Option fixUinputWithAck{this, "FixUinputWithAck", _("Fix Uinput Mode With Ack"), false}; - Option useLotusIcons{this, "UseLotusIcons", _("Use Lotus Status Icons"), false}; - - Option enableDictionary{this, "EnableDictionary", _("Custom Dictionary"), false}; - Option enableCustomKeymap{this, "EnableCustomKeymap", _("Custom Keymap"), false}; - - Option showModeSmooth{this, "ShowModeSmooth", _("Show Uinput (Smooth)"), true}; + Option autoCapitalizeAfterPunctuation{this, "AutoCapitalizeAfterPunctuation", _("Auto capitalize after sentence-ending punctuation (. ! ? Enter)"), false}; + Option doubleSpaceToPeriod{this, "DoubleSpaceToPeriod", _("Double Space to Period"), false}; + Option doubleHyphenToEmDash{this, "DoubleHyphenToEmDash", _("Double Hyphen to Em-Dash (--)"), false}; + Option autoNonVnRestore{this, "AutoNonVnRestore", _("Auto Restore Invalid Words"), true}; + Option modernStyle{this, "ModernStyle", _("Use oà, uý (Instead Of òa, úy)"), true}; + Option freeMarking{this, "FreeMarking", _("Allow Type With More Freedom"), true}; + Option ddFreeStyle{this, "DdFreeStyle", _("Allow dd To Produce đ When Auto Restore Invalid Words Is On"), true}; + Option fixUinputWithAck{this, "FixUinputWithAck", _("Fix Uinput Mode With Ack"), false}; + Option waitSurroundingEvent{this, "WaitSurroundingEvent", _("Experiment: after uinput backspaces, wait for the surrounding-text event instead of sleeping"), false}; + Option waitSurroundingTimeoutMs{this, "WaitSurroundingTimeoutMs", _("Experiment: timeout (ms) for that wait"), 50}; + Option waitSurroundingShortMs{this, "WaitSurroundingShortMs", _("Experiment: shorter timeout (ms) used after two consecutive timeouts without a matching event"), 40}; + Option waitSurroundingProbeEvery{this, "WaitSurroundingProbeEvery", + _("Experiment: when the app's snapshot is frozen during deletion, stop waiting and re-probe once every N replacements"), 4}; + Option waitSurroundingMinPerKeyMs{this, "WaitSurroundingMinPerKeyMs", + _("Experiment: a snapshot identical to the one at send time counts as done only after this many ms per backspace"), 8}; + Option surrDeleteSleepMs{this, "SurrDeleteSleepMs", _("Experiment: ms per deleted char to sleep after deleteSurroundingText"), 4}; + Option surrCommitSleepMs{this, "SurrCommitSleepMs", _("Experiment: ms per committed char to sleep after commitString on the surrounding-text path"), 3}; + Option useLotusIcons{this, "UseLotusIcons", _("Use Lotus Status Icons"), false}; + + Option enableDictionary{this, "EnableDictionary", _("Custom Dictionary"), false}; + Option enableCustomKeymap{this, "EnableCustomKeymap", _("Custom Keymap"), false}; + + Option showModeSmooth{this, "ShowModeSmooth", _("Show Uinput (Smooth)"), true}; Option shortcutSmooth{this, "ShortcutSmooth", _("Shortcut for Uinput (Smooth)"), "1"}; Option showModeUinput{this, "ShowModeUinput", _("Show Uinput (Slow)"), true}; Option shortcutUinput{this, "ShortcutUinput", _("Shortcut for Uinput (Slow)"), "2"}; diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index b30222c0..fd538d87 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -753,10 +753,11 @@ namespace fcitx { } void LotusEngine::deactivate(const InputMethodEntry& /*entry*/, InputContextEvent& event) { - auto* ic = event.inputContext(); - auto* state = ic->propertyFor(&factory_); - const bool surrvalid = ic->surroundingText().isValid(); - const bool is_dbus = getFrontendName(ic) == "dbus"; + auto* ic = event.inputContext(); + auto* state = ic->propertyFor(&factory_); + const bool surrvalid = ic->surroundingText().isValid(); + const bool is_dbus = getFrontendName(ic) == "dbus"; + state->xaChoDangCho(); // v13: chữ còn treo thì giao vào ô CŨ trước khi rời đi state->lastDeactivateTime_ = now_ms(); if (realMode == LotusMode::Preedit && event.type() != EventType::InputContextFocusOut) { state->commitBuffer(); @@ -780,13 +781,25 @@ namespace fcitx { void LotusEngine::refreshEngine() { if (!factory_.registered()) return; - instance_->inputContextManager().foreach ([this](InputContext* ic) { + bool coCuaSoDangGo = false; + instance_->inputContextManager().foreach ([this, &coCuaSoDangGo](InputContext* ic) { auto* state = ic->propertyFor(&factory_); state->setEngine(); - if (ic->hasFocus()) + if (ic->hasFocus()) { + // Đặt lại chế độ theo ĐÚNG luật của app đang gõ. Trước đây setEngine() + // ghi thẳng mặc định chung vào realMode nên nạp lại cấu hình là mất luật + // riêng của cửa sổ đang hoạt động. + setMode(getAppRule(getProgramName(ic)), ic); state->reset(); + coCuaSoDangGo = true; + } return true; }); + // Không có cửa sổ nào đang gõ thì không có luật riêng nào để theo; giữ mặc định + // chung như hành vi cũ. Lần focus kế tiếp activate() sẽ đặt lại cho đúng. + if (!coCuaSoDangGo) { + realMode = config_.mode.value(); + } } void LotusEngine::refreshOption() { diff --git a/src/lotus-state.cpp b/src/lotus-state.cpp index 600424c0..0ba075af 100644 --- a/src/lotus-state.cpp +++ b/src/lotus-state.cpp @@ -41,7 +41,12 @@ namespace fcitx { void LotusState::setEngine() { lotusEngine_.reset(); - realMode = engine_->config().mode.value(); + // KHÔNG đặt realMode ở đây. setEngine() chạy trong hàm dựng của MỌI LotusState và + // trong refreshEngine() cho MỌI input context — cả hai đều không biết cửa sổ nào + // đang gõ. Ghi giá trị mặc định chung vào đây là thổi bay luật theo app của cửa sổ + // đang hoạt động, cho tới lần đổi focus kế tiếp. + // Chế độ do activate() đặt qua setMode(getAppRule(appName), ic), và refreshEngine() + // đặt lại cho đúng input context đang có focus. if (engine_->config().inputMethod.value() == "Custom") { const auto& keymaps = *engine_->customKeymap().customKeymap; @@ -437,6 +442,89 @@ namespace fcitx { ic_->updateUserInterface(UserInterfaceComponent::InputPanel); } + bool LotusState::oDaXoaXong() const { + const auto& s = ic_->surroundingText(); + if (!s.isValid()) { + return false; + } + const std::string& t = s.text(); + // v4: ảnh y hệt ảnh lúc bắn phím xoá thì chưa thể là "xoá xong" — ở nhịp 20 ms ảnh cũ tụt + // lại hai phím trông giống hệt trạng thái sau khi xoá (đo được: 'đươợ'). + if (!cho_anh_luc_gui_.empty() && t + "\x1f" + std::to_string(s.cursor()) == cho_anh_luc_gui_) { + // v8: ở 5 ms ảnh lúc bắn LUÔN chậm một phím nên trông y hệt trạng thái xong, và Firefox không + // gửi trạng thái trung gian — đo được 37/52 lần quá hạn là tin đúng bị gạt, mỗi lần 200 ms. + // Nội dung không phân biệt được; dùng thời gian: đã chờ đủ ngưỡng Slow (8 ms × phím xoá, + // đo 60/60) thì giao không kém an toàn hơn Slow. Kiểm "ngay" (0 ms) vẫn bị chặn như v4. + const auto da_cho = (::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_) / 1000; + const auto toi_thieu = static_cast(engine_->config().waitSurroundingMinPerKeyMs.value()) * static_cast(std::max(expected_backspaces_, 1)); + if (da_cho < toi_thieu) { + return false; + } + } + auto it = t.begin(); + for (unsigned int i = 0; i < s.cursor() && it != t.end(); ++i) { + it = utf8::nextChar(it); + } + const std::string before(t.begin(), it); + auto endsWith = [](const std::string& a, const std::string& b) { return a.size() >= b.size() && a.compare(a.size() - b.size(), b.size(), b) == 0; }; + if (!cho_deleted_.empty() && endsWith(before, cho_prefix_ + cho_deleted_)) { + return false; // ảnh cũ: phần cần xoá vẫn còn + } + return endsWith(before, cho_prefix_); + } + + // v13: đường chờ sự kiện trả vòng lặp về NGAY, nên người dùng kịp đổi cửa sổ trong lúc chữ + // còn treo. deactivate() tắt is_deleting_ ⇒ đồng hồ nổ sau đó thấy cờ tắt là bỏ chữ không một + // lời (đo được: thanh địa chỉ Edge còn 't' đáng lẽ 'tô'). Đường ngủ cũ không có khe này vì nó + // ngủ ngay trong hàm xử lý phím. Giao nốt trước khi rời ô. + void LotusState::xaChoDangCho() { + if (!cho_dang_cho_) { + return; + } + ketThucThayChu("mat tieu diem", false); + } + + void LotusState::ketThucThayChu(const char* ly_do, bool tu_timer) { + const auto tre_ms = (::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_) / 1000; + LOTUS_INFO("Surr wait " + std::string(ly_do) + " after " + std::to_string(tre_ms) + " ms"); + if (std::string(ly_do) == "qua han") { + cho_anh_tin_cay_ = false; + ++cho_qua_han_lien_tiep_; + // v11: đòi ≥ 2 tin và KHÔNG tin nào khác ảnh lúc bắn — Firefox tụt thật thường im hẳn + // (0 tin) hoặc gửi ảnh đang nhích, không tính là đóng băng. + if (cho_so_tin_ >= 2 && !cho_tin_khac_ && cho_anh_gui_cap_nhat_) { + if (++cho_dong_bang_lien_tiep_ >= 2 && !cho_dong_bang_) { + cho_dong_bang_ = true; + cho_dem_tham_do_ = 0; + LOTUS_INFO("Surr frozen: stop waiting"); + } + } else { + cho_dong_bang_lien_tiep_ = 0; + } + } else if (std::string(ly_do) == "su kien" || std::string(ly_do) == "nguong") { + cho_anh_tin_cay_ = true; + cho_qua_han_lien_tiep_ = 0; + cho_dong_bang_lien_tiep_ = 0; + if (cho_dong_bang_) { + cho_dong_bang_ = false; + LOTUS_INFO("Surr frozen: resume waiting"); + } + } + cho_dang_cho_ = false; + if (!tu_timer && cho_surr_timer_) { + cho_surr_timer_.reset(); // không reset từ trong chính callback của nó + } + if (!pending_commit_string_.empty()) { + ic_->commitString(pending_commit_string_); + LOTUS_INFO("Commit: " + pending_commit_string_); + } + expected_backspaces_ = 0; + current_backspace_count_ = 0; + pending_commit_string_.clear(); + is_deleting_.store(false); + replayBufferedKeys(); + } + bool LotusState::handleUInputKeyPress(KeyEvent& event, KeySym currentSym, int sleepTime) { if (!is_deleting_.load()) { return false; @@ -446,10 +534,107 @@ namespace fcitx { if (current_backspace_count_ < expected_backspaces_) { return false; // Allow intermediate backspaces to reach the app to clear autofill/old text. } - std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime * (expected_backspaces_ - 1))); + // v7: app khai "có surrounding text" nhưng gửi ảnh RỖNG (Konsole: valid=1 len=0 suốt) thì + // không tin nào khớp được, chờ chỉ tốn trọn hạn mỗi dấu → đi đường ngủ cũ ở dưới. + const bool anh_rong = ic_->surroundingText().text().empty(); + if (engine_->config().waitSurroundingEvent.value() && anh_rong) { + LOTUS_INFO("Surr wait skip: empty snapshot"); + } + bool bo_cho_dong_bang = false; + if (engine_->config().waitSurroundingEvent.value() && !anh_rong && cho_dong_bang_) { + const int moi = std::max(engine_->config().waitSurroundingProbeEvery.value(), 1); + ++cho_dem_tham_do_; + if (cho_dem_tham_do_ % moi != 0) { + bo_cho_dong_bang = true; + } + } + if (engine_->config().waitSurroundingEvent.value() && !anh_rong && !bo_cho_dong_bang) { + // fcitx5 chỉ có MỘT vòng lặp sự kiện, nên ngủ + thử lại ở dưới + // không bao giờ thấy được tin "ô đã đổi" đến trong lúc ngủ. Ở đây trả vòng lặp + // về ngay, đăng ký nghe InputContextSurroundingTextUpdated ĐÚNG LÚC NÀY (v1 để + // người nghe cũ sống qua lần thay sau → bắt tin trễ của phím trước → giao sớm → + // 0/15). Kiểm bằng NỘI DUNG (oDaXoaXong), không dùng realtextLen. Quá hạn thì + // giao như cũ. Phím người gõ trong lúc chờ vẫn vào buffered_keys_. + event.filterAndAccept(); + cho_surr_bat_dau_ = ::fcitx::now(CLOCK_MONOTONIC); + // v6: sau một lần quá hạn thì Firefox đang tụt lại, ảnh không đáng tin → bỏ kiểm ngay + if (cho_anh_tin_cay_ && oDaXoaXong()) { + LOTUS_INFO("Skip retry"); + ketThucThayChu("ngay", false); + return true; + } + auto* instance = engine_->instance(); + cho_dang_cho_ = true; + cho_so_tin_ = 0; + cho_tin_khac_ = false; + cho_surr_watcher_.reset(); // ngoài dispatch của nó, an toàn + cho_surr_watcher_ = instance->watchEvent(EventType::InputContextSurroundingTextUpdated, EventWatcherPhase::Default, [this](Event& e) { + auto& ice = static_cast(e); + if (!cho_dang_cho_ || ice.inputContext() != ic_ || !is_deleting_.load()) { + return; + } + // v9: vừa quá hạn = app đang tụt, ảnh nó báo là bộ đệm cập nhật trễ ('trươnờ': tin + // 0 ms hiện 'trư' là ảnh cũ vừa bắt kịp, không phải xoá xong). Lúc đó KHÔNG tin tin nào + // tới trước ngưỡng Slow (8 ms × phím xoá). + const auto da_cho_us = ::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_; + const auto toi_thieu_us = + static_cast(engine_->config().waitSurroundingMinPerKeyMs.value()) * static_cast(std::max(expected_backspaces_, 1)) * 1000ULL; + { + const auto& sd0 = ic_->surroundingText(); + ++cho_so_tin_; + if (sd0.text() + "\x1f" + std::to_string(sd0.cursor()) != cho_anh_luc_gui_) { + cho_tin_khac_ = true; + } + } + if (!cho_anh_tin_cay_ && da_cho_us < toi_thieu_us) { + // v9: vừa quá hạn xong thì tin tới sớm là bộ đệm cũ bắt kịp, không tin. + } else if (oDaXoaXong()) { + ketThucThayChu("su kien", false); + } + // Ảnh chưa khớp thì im lặng chờ tiếp: mọi thứ đáng in ở đây đều là chữ + // người dùng vừa gõ, không đưa vào nhật ký. + }); + // v7: hai lần quá hạn liên tiếp mà chưa có tin khớp = app này không cập nhật ảnh trong lúc + // xoá (Edge thanh địa chỉ: ảnh chỉ đổi khi gõ chữ thường) → rút hạn xuống mức ngắn, đủ + // trên cửa sổ vượt mặt (Slow 8 ms/phím đo 60/60); có tin khớp thì trả hạn dài lại. + const int han_ms = cho_qua_han_lien_tiep_ >= 2 ? engine_->config().waitSurroundingShortMs.value() : engine_->config().waitSurroundingTimeoutMs.value(); + const auto han = static_cast(han_ms) * 1000ULL; + // v7.1: accuracy 0 với sd-event = MẶC ĐỊNH 250 ms (đồng hồ được phép nổ muộn tới 250 ms): + // đo được: hạn 40 nổ ở 64, hạn 200 nổ ở 243, tệ nhất 430. Đặt 1 ms. + // v10: 16/27 lần quá hạn còn lại của v9 là tin "xong" tới TRƯỚC ngưỡng Slow rồi app im + // luôn → bị gạt và chờ trọn 200 ms. Đặt thêm một mốc đúng tại ngưỡng: kiểm lại ảnh mới nhất + // đã nhận, khớp thì giao ngay ("nguong"); chưa thì dời đồng hồ tới hạn. + const auto nguong = + static_cast(engine_->config().waitSurroundingMinPerKeyMs.value()) * static_cast(std::max(expected_backspaces_, 1)) * 1000ULL; + const auto moc_dau = nguong < han ? cho_surr_bat_dau_ + nguong : cho_surr_bat_dau_ + han; + cho_surr_timer_ = instance->eventLoop().addTimeEvent(CLOCK_MONOTONIC, moc_dau, 1000, [this, han](EventSourceTime* t, uint64_t) { + if (!cho_dang_cho_ || !is_deleting_.load()) { + return false; + } + const auto da_cho = ::fcitx::now(CLOCK_MONOTONIC) - cho_surr_bat_dau_; + if (da_cho + 1000 < han) { + if (oDaXoaXong()) { + ketThucThayChu("nguong", true); + return false; + } + t->setTime(cho_surr_bat_dau_ + han); + t->setOneShot(); + return true; + } + ketThucThayChu("qua han", true); + return false; + }); + return true; + } + // v11: đóng băng → đường ngủ cũ nhưng với hằng số của Slow (8 × (N − 1), đo 60/60 trên + // Firefox), không ngủ chồng: đo được: ngủ thêm 8 × N làm N=1 mất 24 ms, N=2 mất 34 ms. + const int ngu_moi_phim = bo_cho_dong_bang ? std::max(sleepTime, engine_->config().waitSurroundingMinPerKeyMs.value()) : sleepTime; + std::this_thread::sleep_for(std::chrono::milliseconds(ngu_moi_phim * (expected_backspaces_ - 1))); // Validate surr cursor pos should match realtextLen after all BS applied const auto& surr = ic_->surroundingText(); - if (surr.isValid() && surr.cursor() == realtextLen.load(std::memory_order_acquire)) { + if (bo_cho_dong_bang) { + LOTUS_INFO("Skip retry (frozen)"); // v12: thử lại 3 × 2 ms là vô ích khi ảnh đóng băng + } else if (surr.isValid() && surr.cursor() == realtextLen.load(std::memory_order_acquire)) { LOTUS_INFO("Skip retry"); } else { // Retry x3 (2 ms each), khi can (chromium,electron,...) @@ -477,9 +662,32 @@ namespace fcitx { void LotusState::performReplacement(const std::string& deletedPart, const std::string& addedPart) { LOTUS_INFO("Perform replacement: " + deletedPart + " -> " + addedPart); //NOLINT - current_backspace_count_ = 0; - pending_commit_string_ = addedPart; - expected_backspaces_ = static_cast(utf8::length(deletedPart)); + current_backspace_count_ = 0; + pending_commit_string_ = addedPart; + expected_backspaces_ = static_cast(utf8::length(deletedPart)); + cho_deleted_ = deletedPart; + { + const auto& sg = ic_->surroundingText(); + cho_anh_luc_gui_ = sg.isValid() ? sg.text() + "\x1f" + std::to_string(sg.cursor()) : std::string(); + } + cho_prefix_ = (oldPreBuffer_.size() >= deletedPart.size()) ? oldPreBuffer_.substr(0, oldPreBuffer_.size() - deletedPart.size()) : std::string(); + { + // v12: ảnh lúc bắn "cập nhật" = phần trước con trỏ kết thúc bằng prefix+deleted. Firefox tụt + // thì ảnh lúc bắn đã chậm (không thấy phần sắp xoá) → không được tính là đóng băng + // (nhịp ngẫu nhiên kích hoạt nhầm 3 lần trên Firefox). + cho_anh_gui_cap_nhat_ = false; + const auto& sg2 = ic_->surroundingText(); + if (sg2.isValid()) { + const std::string& t = sg2.text(); + auto it = t.begin(); + for (unsigned int i = 0; i < sg2.cursor() && it != t.end(); ++i) { + it = utf8::nextChar(it); + } + const std::string truoc(t.begin(), it); + const std::string can = cho_prefix_ + cho_deleted_; + cho_anh_gui_cap_nhat_ = truoc.size() >= can.size() && truoc.compare(truoc.size() - can.size(), can.size(), can) == 0; + } + } const auto& surrounding = ic_->surroundingText(); const std::string surrText = surrounding.text(); bool isSurrText = engine_->config().useSurroundingTextIfPossible.value() && ic_->capabilityFlags().test(CapabilityFlag::SurroundingText) && surrounding.isValid() && @@ -500,11 +708,11 @@ namespace fcitx { if (isSurrText) { ic_->deleteSurroundingText(-expected_backspaces_, expected_backspaces_); LOTUS_INFO("Delete using surrounding text"); - std::this_thread::sleep_for(std::chrono::milliseconds(4 * expected_backspaces_)); + std::this_thread::sleep_for(std::chrono::milliseconds(engine_->config().surrDeleteSleepMs.value() * expected_backspaces_)); if (!pending_commit_string_.empty()) { ic_->commitString(pending_commit_string_); LOTUS_INFO("Commit: " + pending_commit_string_); - std::this_thread::sleep_for(std::chrono::milliseconds(3 * utf8::length(addedPart))); + std::this_thread::sleep_for(std::chrono::milliseconds(engine_->config().surrCommitSleepMs.value() * utf8::length(addedPart))); } expected_backspaces_ = 0; current_backspace_count_ = 0; @@ -808,7 +1016,7 @@ namespace fcitx { if (charsToDelete > 0) { ic->deleteSurroundingText(-static_cast(charsToDelete), static_cast(charsToDelete)); - std::this_thread::sleep_for(std::chrono::milliseconds(4 * charsToDelete)); + std::this_thread::sleep_for(std::chrono::milliseconds(engine_->config().surrDeleteSleepMs.value() * charsToDelete)); } if (!addedPart.empty()) { @@ -1005,7 +1213,9 @@ namespace fcitx { LOTUS_WARN("Cannot connect to uinput server, reconnecting...."); connect_uinput_server(); } - if (current_backspace_count_ >= expected_backspaces_ && is_deleting_.load()) { + // Van an toàn này tắt cờ lặng lẽ ở phím kế tiếp; khi đang chờ sự kiện thì phải bỏ qua, + // nếu không chữ chờ giao bị vứt (v2: " Nam"). + if (!cho_dang_cho_ && current_backspace_count_ >= expected_backspaces_ && is_deleting_.load()) { is_deleting_.store(false); current_backspace_count_ = 0; expected_backspaces_ = 0; diff --git a/src/lotus-state.h b/src/lotus-state.h index 1f1df591..c7741f89 100644 --- a/src/lotus-state.h +++ b/src/lotus-state.h @@ -22,6 +22,11 @@ #include #include #include +#include +#include +#include +#include +#include struct EmojiEntry; @@ -84,6 +89,12 @@ namespace fcitx { friend class EmojiCandidateWord; friend class LotusEngine; + /** + * @brief v13: giao nốt chữ đang treo khi ô nhập sắp mất tiêu điểm. + * Không có nó thì đồng hồ hết hạn sau đó thấy is_deleting_ đã tắt và lặng lẽ bỏ chữ. + */ + void xaChoDangCho(); + private: static constexpr size_t MAX_BUFFERED_KEYS = 50; @@ -126,6 +137,27 @@ namespace fcitx { */ void send_backspace_uinput(int count) const; + // Chờ theo sự kiện thay vì ngủ (xem handleUInputKeyPress) + std::unique_ptr> cho_surr_watcher_; + std::unique_ptr cho_surr_timer_; + uint64_t cho_surr_bat_dau_ = 0; + bool cho_dang_cho_ = false; + std::string cho_prefix_; // phần từ giữ lại sau khi xoá + std::string cho_deleted_; // phần phải biến mất + std::string cho_anh_luc_gui_; // ảnh chụp lúc bắn phím xoá + // v11: ảnh ĐÓNG BĂNG = quá hạn mà mọi tin đều y hệt ảnh lúc bắn (Edge thanh địa chỉ). Hai lần liền + // → bỏ chờ, ngủ 8 ms × phím xoá như Slow; cứ `probeEvery` lần thăm dò lại một lần. + int cho_so_tin_ = 0; // số tin trong lần chờ này + bool cho_tin_khac_ = false; // có tin nào khác ảnh lúc bắn + bool cho_anh_gui_cap_nhat_ = false; // v12: ảnh lúc bắn còn thấy phần sắp xoá + int cho_dong_bang_lien_tiep_ = 0; + bool cho_dong_bang_ = false; + int cho_dem_tham_do_ = 0; + int cho_qua_han_lien_tiep_ = 0; // v7: >=2 thì rút hạn chờ (Edge thanh địa chỉ không bao giờ khớp) + bool cho_anh_tin_cay_ = true; // false sau một lần quá hạn, true lại khi có tin khớp + bool oDaXoaXong() const; + void ketThucThayChu(const char* ly_do, bool tu_timer); + /** * @brief Checks if autofill is certain for surrounding text. * @param s The surrounding text.