Skip to content

Commit 2de4961

Browse files
committed
feat(shortcut): add custom shortcut CRUD and display-only support
Add runtime custom shortcut CRUD (AddCustomShortcut/ModifyCustomShortcut/DeleteCustomShortcut) to KeybindingManager with input validation, conflict handling and commit-failure rollback. Add GetShortcutCommand so the control center can fetch a custom shortcut command separately. Persist runtime custom shortcuts through user-level DConfig plus a custom shortcut subpath registry. Keep Add on full save/create semantics, use updateCustomShortcut for Modify so existing entries update only changed fields, and roll back persisted state when registration or conflict persistence fails. Introduce display-only shortcut configs so modifiable shortcuts with no hotkey remain visible as "None" after their binding is taken. Keep these entries in the runtime map and expose them through ListAllShortcuts. Remove the unused checkConflictForConfig helper after conflict checks stayed on the actual LookupConflictShortcut/registerShortcut paths. 新增自定义快捷键运行时增删改(AddCustomShortcut/ModifyCustomShortcut/DeleteCustomShortcut),包含输入校验、冲突处理和提交失败回滚;新增 GetShortcutCommand,供控制中心单独获取自定义快捷键命令。 通过用户级 DConfig 和自定义快捷键 subpath 注册表持久化运行时自定义快捷键。Add 继续使用完整创建/保存语义,Modify 改用 updateCustomShortcut,仅更新已有条目的变化字段,并在注册或冲突持久化失败时回滚持久化状态。 引入 display-only 快捷键配置,使可修改但无热键的快捷键在绑定被其他快捷键占用后仍能以“无”显示。此类条目保留在运行时映射中,并通过 ListAllShortcuts 暴露。 删除未使用的 checkConflictForConfig 辅助函数,因为真实冲突检测仍走 LookupConflictShortcut/registerShortcut 路径。 Log: feat(shortcut): add custom shortcut CRUD and display-only support Change-Id: Ie59dd3cb4e2575eea15974b9454eefc0ff484bfa
1 parent 3d47be1 commit 2de4961

12 files changed

Lines changed: 1134 additions & 93 deletions

File tree

src/plugin-qt/shortcut/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ set(SHORTCUT_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
2727
set(SHORTCUT_COMMON_SOURCES
2828
${SHORTCUT_SRC_DIR}/core/shortcutmanager.cpp
2929
${SHORTCUT_SRC_DIR}/core/keybindingmanager.cpp
30+
${SHORTCUT_SRC_DIR}/core/customshortcuttransaction.cpp
3031
${SHORTCUT_SRC_DIR}/core/gesturemanager.cpp
3132
${SHORTCUT_SRC_DIR}/core/qkeysequenceconverter.cpp
3233
${SHORTCUT_SRC_DIR}/core/actionexecutor.cpp
3334
${SHORTCUT_SRC_DIR}/core/translationmanager.cpp
3435
${SHORTCUT_SRC_DIR}/config/configloader.cpp
36+
${SHORTCUT_SRC_DIR}/config/customshortcutstore.cpp
3537
${SHORTCUT_SRC_DIR}/backend/abstractkeyhandler.h
3638
${SHORTCUT_SRC_DIR}/backend/abstractgesturehandler.h
3739
${SHORTCUT_SRC_DIR}/backend/specialkeyhandler.cpp
@@ -113,6 +115,12 @@ foreach(SUBDIR_PATH ${SHORTCUT_CONFIG_SUBDIRS})
113115
)
114116
endforeach()
115117

118+
# install base json config
119+
dtk_add_config_meta_files(
120+
APPID "org.deepin.dde.keybinding"
121+
FILES "${CMAKE_CURRENT_SOURCE_DIR}/configs/org.deepin.shortcut.json"
122+
)
123+
116124
foreach(SUBDIR_PATH ${GESTURE_CONFIG_SUBDIRS})
117125
dtk_add_config_meta_files(
118126
APPID "org.deepin.dde.keybinding"

src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.displayswitch/org.deepin.shortcut.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
"value": [
2727
"Meta+P"
2828
],
29-
"serial": 0,
29+
"serial": 1,
3030
"flags": [],
3131
"name": "hotkeys",
3232
"name[zh_CN]": "快捷键组合",
3333
"description": "切换显示模式快捷键组合",
34-
"permissions": "",
34+
"permissions": "readwrite",
3535
"visibility": "private"
3636
},
3737
"triggerType": {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"magic": "dsg.config.meta",
3+
"version": "1.0",
4+
"contents": {
5+
"appId": {
6+
"value": "org.deepin.dde.keybinding",
7+
"serial": 0,
8+
"flags": [],
9+
"name": "appId",
10+
"name[zh_CN]": "提供快捷键应用Id",
11+
"description": "provide shortcut app id",
12+
"permissions": "readwrite",
13+
"visibility": "private"
14+
},
15+
"displayName": {
16+
"value": "",
17+
"serial": 0,
18+
"flags": [],
19+
"name": "displayName",
20+
"name[zh_CN]": "",
21+
"description": "",
22+
"permissions": "readwrite",
23+
"visibility": "private"
24+
},
25+
"hotkeys": {
26+
"value": [],
27+
"serial": 0,
28+
"flags": [],
29+
"name": "hotkeys",
30+
"name[zh_CN]": "快捷键组合",
31+
"description": "打开默认终端快捷键组合",
32+
"permissions": "readwrite",
33+
"visibility": "private"
34+
},
35+
"triggerType": {
36+
"value": 1,
37+
"serial": 0,
38+
"flags": [],
39+
"name": "triggerType",
40+
"name[zh_CN]": "快捷键触发动作类型",
41+
"description": "二进制command类型",
42+
"permissions": "readwrite",
43+
"visibility": "private"
44+
},
45+
"triggerValue": {
46+
"value": [],
47+
"serial": 0,
48+
"flags": [],
49+
"name": "triggerValue",
50+
"name[zh_CN]": "快捷键触发动作",
51+
"description": "",
52+
"permissions": "readwrite",
53+
"visibility": "private"
54+
},
55+
"category": {
56+
"value": "Custom",
57+
"serial": 1,
58+
"flags": [],
59+
"name": "category",
60+
"name[zh_CN]": "快捷键类别(Custom)",
61+
"description": "Custom shortcut category",
62+
"permissions": "readwrite",
63+
"visibility": "private"
64+
},
65+
"enabled": {
66+
"value": true,
67+
"serial": 0,
68+
"flags": [],
69+
"name": "enabled",
70+
"name[zh_CN]": "使能",
71+
"description": "是否启用快捷键",
72+
"permissions": "readwrite",
73+
"visibility": "private"
74+
},
75+
"modifiable": {
76+
"value": true,
77+
"serial": 0,
78+
"flags": [],
79+
"name": "modifiable",
80+
"name[zh_CN]": "能否修改快捷键",
81+
"description": "不能修改快捷键",
82+
"permissions": "readwrite",
83+
"visibility": "private"
84+
}
85+
}
86+
}

src/plugin-qt/shortcut/src/config/configloader.cpp

Lines changed: 157 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// SPDX-License-Identifier: LGPL-3.0-or-later
44

55
#include "configloader.h"
6-
#include "core/shortcutconfig.h"
76

87
#include <algorithm>
98

@@ -24,6 +23,12 @@ const QString CONFIG_SUBPATH_DIR = "/usr/share/deepin/org.deepin.dde.keybinding/
2423

2524
DCORE_USE_NAMESPACE
2625

26+
static DConfig *createDConfig(const QString &name, const QString &subPath, QObject *parent)
27+
{
28+
const QString normalized = CustomShortcutStore::normalizeSubPath(subPath);
29+
return DConfig::create(APP_ID, name, QStringLiteral("/") + normalized, parent);
30+
}
31+
2732
ConfigLoader::ConfigLoader(QObject *parent)
2833
: QObject(parent)
2934
{
@@ -106,22 +111,36 @@ void ConfigLoader::resetHotkeys(const QStringList &ids)
106111
}
107112
}
108113

109-
void ConfigLoader::updateValue(const QString &id, const QString &key, const QVariant &value)
114+
bool ConfigLoader::updateValue(const QString &id, const QString &key, const QVariant &value)
110115
{
111116
if (!m_configs.contains(id)) {
112117
qWarning() << "ConfigLoader: config not found or can not be changed:" << id << key << value;
113-
return;
118+
return false;
114119
}
115120

116121
m_configs[id]->setValue(key, value);
122+
return true;
123+
}
124+
125+
bool ConfigLoader::canUpdateValue(const QString &id) const
126+
{
127+
return m_configs.contains(id);
117128
}
118129

119130
QSet<QString> ConfigLoader::discoverSubPaths()
120131
{
121-
// Accumulate all found subpaths
122132
QSet<QString> foundSubPaths;
133+
foundSubPaths.unite(scanIniSubPaths(CONFIG_SUBPATH_DIR));
134+
foundSubPaths.unite(m_customStore.discoverSubPaths());
123135

124-
QDir regDir(CONFIG_SUBPATH_DIR);
136+
return foundSubPaths;
137+
}
138+
139+
QSet<QString> ConfigLoader::scanIniSubPaths(const QString &dirPath)
140+
{
141+
QSet<QString> foundSubPaths;
142+
143+
QDir regDir(dirPath);
125144
if (!regDir.exists()) {
126145
return foundSubPaths;
127146
}
@@ -132,7 +151,7 @@ QSet<QString> ConfigLoader::discoverSubPaths()
132151
static const QRegularExpression reContinuation(QStringLiteral("\\\\\\s*\\n"));
133152
QStringList iniFiles = regDir.entryList(QStringList() << "*.ini", QDir::Files | QDir::NoDotAndDotDot);
134153
for (const QString &iniFile : iniFiles) {
135-
QString fullPath = QDir(CONFIG_SUBPATH_DIR).absoluteFilePath(iniFile);
154+
QString fullPath = regDir.absoluteFilePath(iniFile);
136155
QFile file(fullPath);
137156
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
138157
qWarning() << "ConfigLoader: Failed to open" << fullPath;
@@ -182,6 +201,122 @@ QSet<QString> ConfigLoader::discoverSubPaths()
182201
return foundSubPaths;
183202
}
184203

204+
bool ConfigLoader::saveCustomShortcut(const KeyConfig &config)
205+
{
206+
const QString subPath = CustomShortcutStore::normalizeSubPath(config.subPath);
207+
DConfig *customDconfig = m_configs.value(subPath);
208+
const bool existingDConfig = customDconfig;
209+
if (!customDconfig) {
210+
customDconfig = m_customStore.createConfig(subPath, this);
211+
if (!customDconfig)
212+
return false;
213+
if (!customDconfig->isValid()) {
214+
qWarning() << "ConfigLoader: failed to create custom shortcut DConfig:" << subPath;
215+
delete customDconfig;
216+
return false;
217+
}
218+
}
219+
220+
KeyConfig storedConfig = config;
221+
storedConfig.subPath = subPath;
222+
if (!m_customStore.save(storedConfig, customDconfig)) {
223+
if (!existingDConfig) {
224+
m_customStore.reset(customDconfig, subPath);
225+
customDconfig->deleteLater();
226+
}
227+
return false;
228+
}
229+
230+
auto existing = std::find_if(m_keys.begin(), m_keys.end(),
231+
[&](const KeyConfig &item) { return item.subPath == subPath; });
232+
if (existing != m_keys.end())
233+
*existing = storedConfig;
234+
else
235+
m_keys.append(storedConfig);
236+
237+
m_loadedSubPaths.insert(subPath);
238+
if (!m_configs.contains(subPath)) {
239+
connect(customDconfig, &DConfig::valueChanged, this, [this, subPath, customDconfig](const QString &key) {
240+
if (!customDconfig->isValid() || !m_configs.contains(subPath)) {
241+
qWarning() << "DConfig invalid or not found:" << subPath;
242+
return;
243+
}
244+
245+
KeyConfig updatedConfig = parseKeyConfig(customDconfig);
246+
qDebug() << "DConfig value changed:" << subPath << key;
247+
auto existing = std::find_if(m_keys.begin(), m_keys.end(),
248+
[&](const KeyConfig &item) { return item.subPath == subPath; });
249+
if (existing != m_keys.end()) {
250+
*existing = updatedConfig;
251+
} else {
252+
m_keys.append(updatedConfig);
253+
}
254+
emit keyConfigChanged(updatedConfig);
255+
});
256+
m_configs.insert(subPath, customDconfig);
257+
}
258+
259+
return true;
260+
}
261+
262+
bool ConfigLoader::updateCustomShortcut(const KeyConfig &config)
263+
{
264+
const QString subPath = CustomShortcutStore::normalizeSubPath(config.subPath);
265+
DConfig *customDconfig = m_configs.value(subPath);
266+
if (!customDconfig) {
267+
qWarning() << "ConfigLoader: custom shortcut config not found for update:" << subPath;
268+
return false;
269+
}
270+
271+
KeyConfig storedConfig = config;
272+
storedConfig.subPath = subPath;
273+
if (!m_customStore.updateCustomShortcutFields(storedConfig, customDconfig))
274+
return false;
275+
276+
auto existing = std::find_if(m_keys.begin(), m_keys.end(),
277+
[&](const KeyConfig &item) { return item.subPath == subPath; });
278+
if (existing != m_keys.end())
279+
*existing = storedConfig;
280+
else
281+
m_keys.append(storedConfig);
282+
283+
m_loadedSubPaths.insert(subPath);
284+
return true;
285+
}
286+
287+
bool ConfigLoader::removeCustomShortcut(const QString &subPath)
288+
{
289+
const QString normalized = CustomShortcutStore::normalizeSubPath(subPath);
290+
if (normalized.isEmpty()) {
291+
return false;
292+
}
293+
294+
DConfig *config = m_configs.take(normalized);
295+
if (!config || !config->isValid()) {
296+
qWarning() << "ConfigLoader: custom shortcut config not found for removal:" << subPath;
297+
if (config) config->deleteLater();
298+
return false;
299+
} else {
300+
disconnect(config, nullptr, this, nullptr);
301+
}
302+
303+
m_customStore.reset(config, normalized);
304+
if (!m_customStore.removeSubPath(normalized)) {
305+
qWarning() << "ConfigLoader: failed to remove custom shortcut subPath after reset,"
306+
<< "leaving a stale ini entry:" << normalized;
307+
}
308+
309+
m_loadedSubPaths.remove(normalized);
310+
m_keys.erase(std::remove_if(m_keys.begin(), m_keys.end(),
311+
[&](const KeyConfig &config) { return config.subPath == normalized; }),
312+
m_keys.end());
313+
314+
if (config)
315+
config->deleteLater();
316+
317+
return true;
318+
}
319+
185320
void ConfigLoader::loadConfig(const QString &subPath, bool newOne)
186321
{
187322
qDebug() << "Loading config from:" << subPath;
@@ -199,9 +334,8 @@ void ConfigLoader::loadConfig(const QString &subPath, bool newOne)
199334
return;
200335
}
201336

202-
DConfig *config = DConfig::create(APP_ID,
203-
isKey ? CONFIG_NAME_SHORTCUT : CONFIG_NAME_GESTURE,
204-
"/" + subPath, this);
337+
DConfig *config = createDConfig(isKey ? CONFIG_NAME_SHORTCUT : CONFIG_NAME_GESTURE,
338+
subPath, this);
205339
if (!config->isValid()) {
206340
qWarning() << "Failed to create DConfig for" << subPath;
207341
delete config;
@@ -272,20 +406,22 @@ void ConfigLoader::loadConfig(const QString &subPath, bool newOne)
272406
qDebug() << "DConfig value changed:" << subPath << key;
273407
if (isKey) {
274408
KeyConfig updatedConfig = parseKeyConfig(config);
275-
for (auto &existing : m_keys) {
276-
if (existing.subPath == subPath) {
277-
existing = updatedConfig;
278-
break;
279-
}
409+
auto existing = std::find_if(m_keys.begin(), m_keys.end(),
410+
[&](const KeyConfig &item) { return item.subPath == subPath; });
411+
if (existing != m_keys.end()) {
412+
*existing = updatedConfig;
413+
} else {
414+
m_keys.append(updatedConfig);
280415
}
281416
emit keyConfigChanged(updatedConfig);
282417
} else {
283418
GestureConfig updatedConfig = parseGestureConfig(config);
284-
for (auto &existing : m_gestures) {
285-
if (existing.subPath == subPath) {
286-
existing = updatedConfig;
287-
break;
288-
}
419+
auto existing = std::find_if(m_gestures.begin(), m_gestures.end(),
420+
[&](const GestureConfig &item) { return item.subPath == subPath; });
421+
if (existing != m_gestures.end()) {
422+
*existing = updatedConfig;
423+
} else {
424+
m_gestures.append(updatedConfig);
289425
}
290426
emit gestureConfigChanged(updatedConfig);
291427
}
@@ -295,15 +431,10 @@ void ConfigLoader::loadConfig(const QString &subPath, bool newOne)
295431
}
296432
}
297433

298-
static QString normalizedSubpath(const QString &raw)
299-
{
300-
return raw.startsWith(QLatin1Char('/')) ? raw.mid(1) : raw;
301-
}
302-
303434
KeyConfig ConfigLoader::parseKeyConfig(DConfig *config)
304435
{
305436
KeyConfig keyConfig;
306-
keyConfig.subPath = normalizedSubpath(config->subpath());
437+
keyConfig.subPath = CustomShortcutStore::normalizeSubPath(config->subpath());
307438
keyConfig.appId = config->value("appId").toString();
308439
keyConfig.displayName = config->value("displayName").toString();
309440
keyConfig.enabled = config->value("enabled").toBool();
@@ -324,7 +455,7 @@ KeyConfig ConfigLoader::parseKeyConfig(DConfig *config)
324455
GestureConfig ConfigLoader::parseGestureConfig(DConfig *config)
325456
{
326457
GestureConfig gestureConfig;
327-
gestureConfig.subPath = normalizedSubpath(config->subpath());
458+
gestureConfig.subPath = CustomShortcutStore::normalizeSubPath(config->subpath());
328459
gestureConfig.appId = config->value("appId").toString();
329460
gestureConfig.displayName = config->value("displayName").toString();
330461
gestureConfig.enabled = config->value("enabled").toBool();

0 commit comments

Comments
 (0)