diff --git a/src/frontend/dbusfrontend/dbusfrontend.cpp b/src/frontend/dbusfrontend/dbusfrontend.cpp index ade9b761c..1c5b99311 100644 --- a/src/frontend/dbusfrontend/dbusfrontend.cpp +++ b/src/frontend/dbusfrontend/dbusfrontend.cpp @@ -27,6 +27,7 @@ #include "fcitx-utils/log.h" #include "fcitx-utils/misc_p.h" #include "fcitx-utils/rect.h" +#include "fcitx-utils/utf8.h" #include "fcitx/addonfactory.h" #include "fcitx/addoninstance.h" #include "fcitx/candidatelist.h" @@ -120,6 +121,7 @@ class InputMethod1 : public dbus::ObjectVTable { }; class DBusInputContext1 : public InputContext, + public AtomicSurroundingTextInputContext, public dbus::ObjectVTable { public: DBusInputContext1(int id, InputContextManager &icManager, InputMethod1 *im, @@ -172,6 +174,24 @@ class DBusInputContext1 : public InputContext, const dbus::ObjectPath &path() const { return path_; } + bool supportsAtomicSurroundingTextReplacement() const override { + return blocked_; + } + + bool replaceSurroundingTextAtomically( + int offset, unsigned int size, const std::string &text) override { + if (!blocked_ || !utf8::validate(text)) { + return false; + } + blockedEvents_.emplace_back( + BATCHED_DELETE_SURROUNDING, + dbus::DBusStruct(offset, size)); + if (!text.empty()) { + blockedEvents_.emplace_back(BATCHED_COMMIT_STRING, text); + } + return true; + } + void updateIM(const InputMethodEntry *entry) { currentIMTo(name_, entry->name(), entry->uniqueName(), entry->languageCode()); diff --git a/src/frontend/waylandim/virtualinputcontext.h b/src/frontend/waylandim/virtualinputcontext.h index 89f7961b9..acb4f9789 100644 --- a/src/frontend/waylandim/virtualinputcontext.h +++ b/src/frontend/waylandim/virtualinputcontext.h @@ -21,7 +21,8 @@ namespace fcitx { class VirtualInputContextManager; -class VirtualInputContextGlue : public InputContext { +class VirtualInputContextGlue : public InputContext, + public AtomicSurroundingTextInputContext { public: using InputContext::InputContext; // Qualifier is const to ensure the state is read from ic. @@ -32,6 +33,20 @@ class VirtualInputContextGlue : public InputContext { virtual void forwardKeyDelegate(InputContext *ic, const ForwardKeyEvent &key) const = 0; virtual void updatePreeditDelegate(InputContext *ic) const = 0; + virtual bool supportsAtomicSurroundingTextReplacementDelegate( + const InputContext *ic) const = 0; + virtual bool replaceSurroundingTextAtomicallyDelegate( + InputContext *ic, int offset, unsigned int size, + const std::string &text) const = 0; + + bool supportsAtomicSurroundingTextReplacement() const final { + return supportsAtomicSurroundingTextReplacementDelegate(this); + } + bool replaceSurroundingTextAtomically( + int offset, unsigned int size, const std::string &text) final { + return replaceSurroundingTextAtomicallyDelegate( + this, offset, size, text); + } bool realFocus() const { if (virtualICManager_) { @@ -70,7 +85,8 @@ class VirtualInputContextGlue : public InputContext { VirtualInputContextManager *virtualICManager_ = nullptr; }; -class VirtualInputContext : public InputContext { +class VirtualInputContext : public InputContext, + public AtomicSurroundingTextInputContext { public: VirtualInputContext(InputContextManager &manager, const std::string &program, @@ -86,6 +102,15 @@ class VirtualInputContext : public InputContext { const char *frontend() const override { return parent_->frontend(); } InputContext *parent() const { return parent_; } + bool supportsAtomicSurroundingTextReplacement() const final { + return parent_->supportsAtomicSurroundingTextReplacementDelegate( + this); + } + bool replaceSurroundingTextAtomically( + int offset, unsigned int size, const std::string &text) final { + return parent_->replaceSurroundingTextAtomicallyDelegate( + this, offset, size, text); + } protected: void commitStringImpl(const std::string &text) override { diff --git a/src/frontend/waylandim/waylandimserver.cpp b/src/frontend/waylandim/waylandimserver.cpp index 08a966540..7c16b47f8 100644 --- a/src/frontend/waylandim/waylandimserver.cpp +++ b/src/frontend/waylandim/waylandimserver.cpp @@ -660,6 +660,43 @@ void WaylandIMInputContextV1::updatePreeditDelegate(InputContext *ic) const { preeditCommitString.c_str()); } +bool WaylandIMInputContextV1:: +supportsAtomicSurroundingTextReplacementDelegate( + const InputContext * /*ic*/) const { + return static_cast(ic_); +} + +bool WaylandIMInputContextV1::replaceSurroundingTextAtomicallyDelegate( + InputContext *ic, int offset, unsigned int size, + const std::string &text) const { + if (!ic_ || !utf8::validate(text)) { + return false; + } + const size_t cursor = ic->surroundingText().cursor(); + if (static_cast(cursor) + offset < 0) { + return false; + } + const auto &surrounding = ic->surroundingText().text(); + const auto len = utf8::length(surrounding); + const size_t start = cursor + offset; + const size_t end = start + size; + if (cursor > len || start > len || end > len) { + return false; + } + const auto startBytes = utf8::ncharByteLength(surrounding.begin(), start); + const auto cursorBytes = + utf8::ncharByteLength(surrounding.begin(), cursor); + const auto sizeBytes = + utf8::ncharByteLength(surrounding.begin() + startBytes, size); + ic_->deleteSurroundingText(startBytes - cursorBytes, sizeBytes); + bool committed = false; + WaylandIMServerBase::commitStringWrapper(text, [&](const char *str) { + ic_->commitString(serial_, str); + committed = true; + }); + return committed; +} + void WaylandIMInputContextV1::deleteSurroundingTextDelegate( InputContext *ic, int offset, unsigned int size) const { if (!ic_) { diff --git a/src/frontend/waylandim/waylandimserver.h b/src/frontend/waylandim/waylandimserver.h index 2b730a443..9357b5566 100644 --- a/src/frontend/waylandim/waylandimserver.h +++ b/src/frontend/waylandim/waylandimserver.h @@ -99,6 +99,11 @@ class WaylandIMInputContextV1 : public VirtualInputContextGlue { } void deleteSurroundingTextDelegate(InputContext *ic, int offset, unsigned int size) const override; + bool supportsAtomicSurroundingTextReplacementDelegate( + const InputContext *ic) const override; + bool replaceSurroundingTextAtomicallyDelegate( + InputContext *ic, int offset, unsigned int size, + const std::string &text) const override; void forwardKeyDelegate(InputContext *ic, const ForwardKeyEvent &key) const override { FCITX_UNUSED(ic); diff --git a/src/frontend/waylandim/waylandimserverv2.cpp b/src/frontend/waylandim/waylandimserverv2.cpp index 271583479..e767cd090 100644 --- a/src/frontend/waylandim/waylandimserverv2.cpp +++ b/src/frontend/waylandim/waylandimserverv2.cpp @@ -694,6 +694,49 @@ void WaylandIMInputContextV2::updatePreeditDelegate(InputContext *ic) const { ic_->commit(serial_); } +bool WaylandIMInputContextV2:: +supportsAtomicSurroundingTextReplacementDelegate( + const InputContext * /*ic*/) const { + return realFocus() && static_cast(ic_); +} + +bool WaylandIMInputContextV2::replaceSurroundingTextAtomicallyDelegate( + InputContext *ic, int offset, unsigned int size, + const std::string &text) const { + if (!realFocus() || !ic_ || !utf8::validate(text) || offset > 0 || + offset + static_cast(size) < 0) { + return false; + } + const size_t cursor = ic->surroundingText().cursor(); + if (static_cast(cursor) + offset < 0) { + return false; + } + const auto &surrounding = ic->surroundingText().text(); + const auto len = utf8::length(surrounding); + const size_t start = cursor + offset; + const size_t end = start + size; + if (cursor > len || start > len || end > len) { + return false; + } + const auto startBytes = utf8::ncharByteLength(surrounding.begin(), start); + const auto cursorBytes = + utf8::ncharByteLength(surrounding.begin(), cursor); + const auto sizeBytes = + utf8::ncharByteLength(surrounding.begin() + startBytes, size); + ic_->deleteSurroundingText(cursorBytes - startBytes, + startBytes + sizeBytes - cursorBytes); + bool committed = false; + WaylandIMServerBase::commitStringWrapper(text, [&](const char *str) { + ic_->commitString(str); + committed = true; + }); + if (!committed) { + return false; + } + ic_->commit(serial_); + return true; +} + void WaylandIMInputContextV2::deleteSurroundingTextDelegate( InputContext *ic, int offset, unsigned int size) const { if (!realFocus()) { diff --git a/src/frontend/waylandim/waylandimserverv2.h b/src/frontend/waylandim/waylandimserverv2.h index 6378b8a85..0e78220bb 100644 --- a/src/frontend/waylandim/waylandimserverv2.h +++ b/src/frontend/waylandim/waylandimserverv2.h @@ -111,6 +111,11 @@ class WaylandIMInputContextV2 : public VirtualInputContextGlue { } void deleteSurroundingTextDelegate(InputContext *ic, int offset, unsigned int size) const override; + bool supportsAtomicSurroundingTextReplacementDelegate( + const InputContext *ic) const override; + bool replaceSurroundingTextAtomicallyDelegate( + InputContext *ic, int offset, unsigned int size, + const std::string &text) const override; void forwardKeyDelegate(InputContext * /*ic*/, const ForwardKeyEvent &key) const override; diff --git a/src/lib/fcitx/inputcontext.cpp b/src/lib/fcitx/inputcontext.cpp index fe01f5f9d..aceaf0c41 100644 --- a/src/lib/fcitx/inputcontext.cpp +++ b/src/lib/fcitx/inputcontext.cpp @@ -39,6 +39,9 @@ namespace fcitx { +AtomicSurroundingTextInputContext::~AtomicSurroundingTextInputContext() = + default; + namespace { bool shouldDisablePreeditByDefault(const std::string &program) { diff --git a/src/lib/fcitx/inputcontext.h b/src/lib/fcitx/inputcontext.h index 463aeb47e..33174d0e9 100644 --- a/src/lib/fcitx/inputcontext.h +++ b/src/lib/fcitx/inputcontext.h @@ -42,6 +42,15 @@ class InputPanel; class StatusArea; using InputContextVisitor = std::function; +/** Optional frontend contract for one-transaction surrounding replacement. */ +class FCITXCORE_EXPORT AtomicSurroundingTextInputContext { +public: + virtual ~AtomicSurroundingTextInputContext(); + virtual bool supportsAtomicSurroundingTextReplacement() const = 0; + virtual bool replaceSurroundingTextAtomically( + int offset, unsigned int size, const std::string &text) = 0; +}; + /** * An input context represents a client of Fcitx. It can be a Window, or a text * field depending on the application.