From 946a2882608692301ca9ed11f6ff8bcebcb1aa30 Mon Sep 17 00:00:00 2001 From: Wang Yu Date: Thu, 23 Apr 2026 13:41:35 +0800 Subject: [PATCH] fix: sync keyboard layout between DCC and fcitx5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add bidirectional synchronization between DCC keyboard layout settings and fcitx5 input method configuration. 添加DCC键盘布局与fcitx5输入法的双向同步功能。 DCC切换布局时自动同步到fcitx5(添加/排序/设置当前), fcitx5输入法列表变化时同步第一个键盘布局到DCC。 Log: 添加DCC键盘布局与fcitx5输入法双向同步 PMS: BUG-357551 Influence: DCC键盘布局设置与fcitx5输入法配置保持一致, 切换/添加/删除键盘布局时自动同步,支持带变体的键盘布局。 --- .../operation/keyboardcontroller.cpp | 12 +- .../operation/keyboardcontroller.h | 6 +- .../operation/keyboardmodel.cpp | 4 +- .../keyboard-layout/operation/keyboardmodel.h | 6 +- .../operation/fcitx5configtool.cpp | 144 ++++++++++++++++++ .../operation/private/fcitx5configtool_p.h | 8 +- 6 files changed, 172 insertions(+), 8 deletions(-) diff --git a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.cpp b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.cpp index 696a0431..31d17ad0 100644 --- a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.cpp +++ b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.cpp @@ -1,4 +1,4 @@ -//SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +//SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // //SPDX-License-Identifier: GPL-3.0-or-later @@ -161,6 +161,11 @@ bool KeyboardController::userLayoutsContains(const QString &key) return userLayouts().contains(key); } +bool KeyboardController::allLayoutsContains(const QString &key) const +{ + return m_model->allLayout().contains(key); +} + QSortFilterProxyModel *KeyboardController::layoutSearchModel() { if (m_layoutSearchModel) @@ -311,7 +316,11 @@ void KeyboardController::addUserLayout(const QString &layout) void KeyboardController::deleteUserLayout(const QString &layout) { + const auto &userLayout = m_model->userLayout(); + QString key = userLayout.key(layout, layout); + bool isCur = (key == currentLayout()); m_worker->delUserLayout(layout); + Q_EMIT layoutDeleted(key, isCur); } int KeyboardController::layoutCount() const @@ -330,6 +339,7 @@ void KeyboardController::setCurrentLayout(const QString &key) return; m_worker->setLayout(key); + Q_EMIT currentLayoutSet(key); } QString KeyboardController::conflictText() const diff --git a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.h b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.h index cb92364a..8990ea5d 100644 --- a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.h +++ b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardcontroller.h @@ -1,4 +1,4 @@ -//SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +//SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // //SPDX-License-Identifier: GPL-3.0-or-later @@ -58,6 +58,7 @@ public Q_SLOTS: QMap userLayouts() const; QString userLayoutAt(int index, bool isValue = true) const; bool userLayoutsContains(const QString &key); + bool allLayoutsContains(const QString &key) const; QSortFilterProxyModel *layoutSearchModel(); QSortFilterProxyModel *shortcutSearchModel(); @@ -95,6 +96,9 @@ public Q_SLOTS: void keyDone(const QString &accels); void keyEvent(const QString &accels); + void layoutDeleted(const QString &key, bool isCurrentLayout); + void currentLayoutSet(const QString &key); + void conflictTextChanged(); void keyboardEnabledChanged(); diff --git a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.cpp b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.cpp index ffb2fe5c..7b729bcd 100644 --- a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.cpp +++ b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.cpp @@ -1,4 +1,4 @@ -//SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. +//SPDX-FileCopyrightText: 2018 - 2026 UnionTech Software Technology Co., Ltd. // //SPDX-License-Identifier: GPL-3.0-or-later @@ -202,7 +202,7 @@ QString KeyboardModel::curLang() const return langByKey(m_currentLangKey); } -QMap KeyboardModel::userLayout() const +const QMap &KeyboardModel::userLayout() const { return m_userLayout; } diff --git a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.h b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.h index 5b24266b..e6a26a15 100644 --- a/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.h +++ b/src/dcc-fcitx5configtool/keyboard-layout/operation/keyboardmodel.h @@ -1,4 +1,4 @@ -//SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. +//SPDX-FileCopyrightText: 2018 - 2026 UnionTech Software Technology Co., Ltd. // //SPDX-License-Identifier: GPL-3.0-or-later @@ -32,9 +32,9 @@ class KeyboardModel : public QObject QString curLayout() const; QString curLang() const; - QMap userLayout() const; + const QMap &userLayout() const; QMap kbLayout() const; - QMap allLayout() const { return m_allLayouts; } + const QMap &allLayout() const { return m_allLayouts; } QStringList localLang() const; QList langLists() const; bool capsLock() const; diff --git a/src/dcc-fcitx5configtool/operation/fcitx5configtool.cpp b/src/dcc-fcitx5configtool/operation/fcitx5configtool.cpp index 2584bf96..cdf9acac 100644 --- a/src/dcc-fcitx5configtool/operation/fcitx5configtool.cpp +++ b/src/dcc-fcitx5configtool/operation/fcitx5configtool.cpp @@ -30,6 +30,39 @@ Q_LOGGING_CATEGORY(configTool, "fcitx5.configtool.main") namespace deepin { namespace fcitx5configtool { +static QString dccLayoutToFcitxIMKey(const QString &key) { + // DCC: "fr;" or "fr;bepo" -> fcitx5: "keyboard-fr" or "keyboard-fr-bepo" + QString stripped = key; + stripped.replace(';', '-'); + while (stripped.endsWith('-')) stripped.chop(1); + return QStringLiteral("keyboard-") + stripped; +} +static QString fcitxIMKeyToDccLayout(const QString &imKey) { + const QString prefix = QStringLiteral("keyboard-"); + if (!imKey.startsWith(prefix)) return QString(); + // fcitx5: "keyboard-fr-bepo" -> DCC: "fr;bepo" + QString stripped = imKey.mid(prefix.length()); + int dashPos = stripped.indexOf('-'); + if (dashPos < 0) { + return stripped + ';'; + } + return stripped.left(dashPos) + ';' + stripped.mid(dashPos + 1); +} + +static int indexOfEntry(const fcitx::FcitxQtStringKeyValueList &entries, const QString &key) { + for (int i = 0; i < entries.size(); ++i) + if (entries[i].key() == key) return i; + return -1; +} + +struct SyncGuard { + bool &flag; + SyncGuard(bool &f) : flag(f) { flag = true; } + ~SyncGuard() { flag = false; } + SyncGuard(const SyncGuard &) = delete; + SyncGuard &operator=(const SyncGuard &) = delete; +}; + static QString kFcitxConfigGlobalPath = "fcitx://config/global"; #ifdef BUILD_UOS static const QString kSogouAddonUniqueName = "com.sogou.ime.ng.fcitx5.uos-addon"; @@ -70,6 +103,10 @@ void Fcitx5ConfigToolWorkerPrivate::initConnect() if (avail) { configProxy->requestConfig(false); addonsProxy->load(); + connect(dbusProvider->controller(), &fcitx::FcitxQtControllerProxy::InputMethodGroupsChanged, + this, [this]() { + if (!m_syncInProgress) imConfig->load(); + }, Qt::UniqueConnection); } else { configProxy->clear(); addonsProxy->clear(); @@ -94,6 +131,25 @@ void Fcitx5ConfigToolWorkerPrivate::initConnect() connect(configProxy, &Fcitx5ConfigProxy::requestConfigFinished, q, &Fcitx5ConfigToolWorker::requestConfigFinished); connect(addonsProxy, &Fcitx5AddonsProxy::requestAddonsFinished, q, &Fcitx5ConfigToolWorker::requestAddonsFinished); + + connect(keyboardController, &dccV25::KeyboardController::currentLayoutSet, this, + [this](const QString &key) { + if (!m_syncInProgress) syncCurrentLayoutToFcitx5(key); + }); + + connect(keyboardController, &dccV25::KeyboardController::layoutDeleted, this, + [this](const QString &key, bool isCurrent) { + if (!m_syncInProgress && isCurrent) syncLayoutDeletedFromFcitx5(key); + }); + + connect(imConfig, &fcitx::kcm::IMConfig::imListChanged, this, [this]() { + if (!m_syncInProgress) syncFcitx5FirstKeyboardToDCC(); + if (!m_pendingLayoutKey.isEmpty()) { + QString key = m_pendingLayoutKey; + syncCurrentLayoutToFcitx5(key); + } + }); + configProxy->requestConfig(false); addonsProxy->load(); qCDebug(configTool) << "Exiting Fcitx5ConfigToolWorkerPrivate::initConnect"; @@ -244,6 +300,94 @@ fcitx::kcm::IMProxyModel *Fcitx5ConfigToolWorker::imProxyModel() const return d->imConfig->availIMModel(); } +void Fcitx5ConfigToolWorkerPrivate::syncCurrentLayoutToFcitx5(const QString &layoutKey) +{ + if (!dbusProvider || !dbusProvider->controller()) { + qCDebug(configTool) << "syncCurrentLayoutToFcitx5: controller not available, pending:" << layoutKey; + m_pendingLayoutKey = layoutKey; + return; + } + + QString fcitxKey = dccLayoutToFcitxIMKey(layoutKey); + + const auto &entries = imConfig->imEntries(); + int existIndex = indexOfEntry(entries, fcitxKey); + + SyncGuard guard(m_syncInProgress); + m_pendingLayoutKey.clear(); + + if (existIndex > 0) { + qCDebug(configTool) << "syncCurrentLayoutToFcitx5: move to front:" << fcitxKey; + auto updatedEntries = entries; + updatedEntries.move(existIndex, 0); + imConfig->setIMEntries(updatedEntries); + imConfig->emitChanged(); + imConfig->save(); + } else if (existIndex < 0) { + qCDebug(configTool) << "syncCurrentLayoutToFcitx5: prepend:" << fcitxKey; + auto updatedEntries = entries; + fcitx::FcitxQtStringKeyValue newEntry; + newEntry.setKey(fcitxKey); + updatedEntries.prepend(newEntry); + imConfig->setIMEntries(updatedEntries); + imConfig->emitChanged(); + imConfig->save(); + } + + dbusProvider->controller()->SetCurrentIM(fcitxKey); +} + +void Fcitx5ConfigToolWorkerPrivate::syncLayoutDeletedFromFcitx5(const QString &layoutKey) +{ + if (!dbusProvider || !dbusProvider->controller()) return; + + QString fcitxKey = dccLayoutToFcitxIMKey(layoutKey); + const auto &entries = imConfig->imEntries(); + int removedIndex = indexOfEntry(entries, fcitxKey); + if (removedIndex < 0) return; + + qCDebug(configTool) << "syncLayoutDeletedFromFcitx5:" << fcitxKey; + SyncGuard guard(m_syncInProgress); + auto updatedEntries = entries; + updatedEntries.removeAt(removedIndex); + imConfig->setIMEntries(updatedEntries); + imConfig->emitChanged(); + imConfig->save(); + + if (!updatedEntries.isEmpty()) { + dbusProvider->controller()->SetCurrentIM(updatedEntries.first().key()); + } +} + +void Fcitx5ConfigToolWorkerPrivate::syncFcitx5FirstKeyboardToDCC() +{ + if (!keyboardController || !imConfig) return; + + const auto &entries = imConfig->imEntries(); + QString firstKeyboardLayout; + for (const auto &entry : entries) { + QString dccKey = fcitxIMKeyToDccLayout(entry.key()); + if (!dccKey.isEmpty()) { + firstKeyboardLayout = dccKey; + break; + } + } + + if (firstKeyboardLayout.isEmpty()) return; + + if (!keyboardController->allLayoutsContains(firstKeyboardLayout)) { + qCDebug(configTool) << "syncFcitx5FirstKeyboardToDCC: layout not in DCC:" << firstKeyboardLayout; + return; + } + + qCDebug(configTool) << "syncFcitx5FirstKeyboardToDCC:" << firstKeyboardLayout; + SyncGuard guard(m_syncInProgress); + if (!keyboardController->userLayoutsContains(firstKeyboardLayout)) { + keyboardController->addUserLayout(firstKeyboardLayout); + } + keyboardController->setCurrentLayout(firstKeyboardLayout); +} + DCC_FACTORY_CLASS(Fcitx5ConfigToolWorker) } // namespace fcitx5configtool } // namespace deepin diff --git a/src/dcc-fcitx5configtool/operation/private/fcitx5configtool_p.h b/src/dcc-fcitx5configtool/operation/private/fcitx5configtool_p.h index c8a32644..e0c7faca 100755 --- a/src/dcc-fcitx5configtool/operation/private/fcitx5configtool_p.h +++ b/src/dcc-fcitx5configtool/operation/private/fcitx5configtool_p.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 - 2027 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #ifndef FCITX5CONFIGTOOL_P_H @@ -36,7 +36,13 @@ class Fcitx5ConfigToolWorkerPrivate : public QObject IMListModel *imListModel { nullptr }; dccV25::KeyboardController *keyboardController { nullptr }; + bool m_syncInProgress { false }; + QString m_pendingLayoutKey; + private: + void syncCurrentLayoutToFcitx5(const QString &layoutKey); + void syncLayoutDeletedFromFcitx5(const QString &layoutKey); + void syncFcitx5FirstKeyboardToDCC(); void init(); void initConnect();