diff --git a/src/lotus-config.h b/src/lotus-config.h index 6c2177a7..e98b2a45 100644 --- a/src/lotus-config.h +++ b/src/lotus-config.h @@ -241,6 +241,13 @@ namespace fcitx { 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}; diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index 72857bf1..c5422e24 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -777,6 +777,7 @@ namespace fcitx { 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(); diff --git a/src/lotus-state.cpp b/src/lotus-state.cpp index a62df55e..4910b54b 100644 --- a/src/lotus-state.cpp +++ b/src/lotus-state.cpp @@ -436,6 +436,92 @@ 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; @@ -445,10 +531,108 @@ 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,...) @@ -479,6 +663,26 @@ namespace fcitx { 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() && @@ -499,11 +703,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; @@ -807,7 +1011,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()) { @@ -1004,7 +1208,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 f72645b6..6a8ef14d 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; @@ -127,6 +138,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.