Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions src/plugin-qt/shortcut/src/config/configloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,29 @@
// Content changes to existing entries are delivered via DConfig::valueChanged.
}

void ConfigLoader::resetConfig()
QStringList ConfigLoader::resettableHotkeyIds() const

Check warning on line 83 in src/plugin-qt/shortcut/src/config/configloader.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'resettableHotkeyIds' is never used.
{
// Reset key hotkeys to defaults. Only keys are touched — gestures have
// no "hotkeys" field. Other key fields (enabled, keyEventFlags...) are
// left alone, and non-modifiable keys (hardware keys, locked entries)
// are skipped.
QStringList ids;

for (const KeyConfig &key : m_keys) {
if (!key.modifiable) continue;
DConfig *config = m_configs.value(key.subPath);
if (config) config->reset("hotkeys");
if (config && !config->isDefaultValue("hotkeys")) {
ids.append(key.getId());
}
}

return ids;
}

void ConfigLoader::resetHotkeys(const QStringList &ids)

Check warning on line 98 in src/plugin-qt/shortcut/src/config/configloader.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'resetHotkeys' is never used.
{
// Reset key hotkeys to defaults.
for (const QString &id : ids) {
DConfig *config = m_configs.value(id);
if (config) {
config->reset("hotkeys");
}
}
}

Expand Down Expand Up @@ -199,8 +212,10 @@
if (isKey) {
KeyConfig keyConfig = parseKeyConfig(config);
if (!keyConfig.isValid()) {
// isValid() requires enabled && non-empty appId/displayName/hotkeys.
// Most commonly this triggers for shipped-disabled hardware keys
// isValid() requires enabled && non-empty appId/displayName.
// Empty hotkeys are valid: ReplaceHotkey can intentionally leave
// a shortcut unassigned so Reset can restore the default later.
// Most commonly invalid configs are shipped-disabled hardware keys
// (e.g. wlan with enabled=false). Remember the subPath so reload()
// doesn't re-attempt and log this every dpkg trigger.
qWarning() << "Skipping invalid or disabled KeyConfig:" << subPath
Expand Down
3 changes: 2 additions & 1 deletion src/plugin-qt/shortcut/src/config/configloader.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class ConfigLoader : public QObject

void scanForConfigs();
void reload();
void resetConfig();
QStringList resettableHotkeyIds() const;
void resetHotkeys(const QStringList &ids);
void updateValue(const QString &id, const QString &key, const QVariant &value);
void dumpConfigs();

Expand Down
97 changes: 82 additions & 15 deletions src/plugin-qt/shortcut/src/core/keybindingmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
#include "core/shortcutconfig.h"
#include "qkeysequenceconverter.h"

#include <QDebug>

Check warning on line 14 in src/plugin-qt/shortcut/src/core/keybindingmanager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDebug> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QDBusConnection>

Check warning on line 15 in src/plugin-qt/shortcut/src/core/keybindingmanager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDBusConnection> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <algorithm>

Check warning on line 17 in src/plugin-qt/shortcut/src/core/keybindingmanager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <algorithm> not found. Please note: Cppcheck does not need standard library headers to get proper results.

namespace {
constexpr const char *kDefaultShortcutAppId = "org.deepin.dde.keybinding";
constexpr const char *kNoHotkeyText = "None";
}

// Normalize a hotkey from XKB form ("<Control><Alt>T") to Qt PortableText
// ("Ctrl+Alt+T"). Inputs already in Qt form pass through unchanged.
// dde-services emits XKB on the wire for legacy control-center compatibility
Expand All @@ -34,6 +41,12 @@
return out;
}

static bool containsEmptyHotkey(const QStringList &hotkeys)
{
return std::any_of(hotkeys.begin(), hotkeys.end(),
[](const QString &h) { return h.trimmed().isEmpty(); });
}

KeybindingManager::KeybindingManager(ConfigLoader *loader, ActionExecutor *executor,
TranslationManager *translationManager,
AbstractKeyHandler *keyHandler,
Expand Down Expand Up @@ -101,9 +114,7 @@
for (const auto &config : m_keyConfigsMap) {
// Only expose modifiable shortcuts — control center filters out
// non-modifiable entries to avoid showing read-only items.
// Skip shortcuts with no hotkeys (e.g. after ReplaceHotkey stole
// the last binding but before the next reload prunes them).
if (!config.modifiable || config.hotkeys.isEmpty()) {
if (!config.modifiable) {
continue;
}
list.append(toShortcutInfo(config));
Expand Down Expand Up @@ -213,6 +224,11 @@
// Accept either Qt PortableText ("Ctrl+Alt+T") or XKB form
// ("<Control><Alt>T") on the wire; canonicalize to Qt internally.
const QStringList normalized = normalizeHotkeys(newHotkeys);
if (normalized.isEmpty() || containsEmptyHotkey(normalized)) {
qWarning() << "ModifyHotkeys: new hotkeys can not be empty:" << id;
return false;
}

// Check for conflicts (exclude self)
for (const QString &hotkey : normalized) {
ShortcutInfo conflictInfo = LookupConflictShortcut(hotkey);
Expand Down Expand Up @@ -363,6 +379,10 @@
}

const QString normalized = normalizeHotkey(newHotkey);
if (normalized.trimmed().isEmpty()) {
qWarning() << "ReplaceHotkey: new hotkey can not be empty:" << targetId;
return false;
}

// Remove the hotkey from the conflict shortcut
if (!conflictConfig.hotkeys.removeOne(normalized)) {
Expand Down Expand Up @@ -411,14 +431,7 @@
m_loader->updateValue(targetId, "hotkeys", targetConfig.hotkeys);
m_loader->updateValue(conflictId, "hotkeys", conflictConfig.hotkeys);
emit ShortcutChanged(targetId, toShortcutInfo(targetConfig));
if (conflictConfig.hotkeys.isEmpty()) {
// Last hotkey was stolen.
// The shortcut stays in dconfig with empty hotkeys; on next load
// registerShortcut will skip it (isValid requires non-empty hotkeys).
emit ShortcutRemoved(conflictId);
} else {
emit ShortcutChanged(conflictId, toShortcutInfo(conflictConfig));
}
emit ShortcutChanged(conflictId, toShortcutInfo(conflictConfig));

return true;
}
Expand Down Expand Up @@ -467,7 +480,39 @@
void KeybindingManager::Reset()
{
// Reset shortcut hotkeys to defaults; other fields (enabled, etc.) are untouched.
m_loader->resetConfig();
const QStringList resetIds = m_loader->resettableHotkeyIds();
if (resetIds.isEmpty()) {
return;
}

// DConfig resets are delivered later as per-key valueChanged signals.
// Release all reset targets first so one restored default does not conflict
// with another target's stale binding.
QStringList toRestore;
for (const QString &id : resetIds) {
if (!m_keyConfigsMap.contains(id)) {
continue;
}

m_keyHandler->unregisterKey(id);
m_specialKeyHandler->unregisterKey(id);
toRestore.append(id);
}

if (!m_keyHandler->commitSync()) {
// Compositor rejected the unregister; leave the local map and dconfig
// untouched so state stays consistent with what's actually bound.
qWarning() << "Reset: failed to commit unregistering existing hotkeys";
return;
}

// The compositor confirmed the unregister. Drop the local entries so the
// async valueChanged restore goes through onKeyConfigChanged's new-entry path
for (const QString &id : toRestore) {
m_keyConfigsMap.remove(id);
}

m_loader->resetHotkeys(resetIds);
}

void KeybindingManager::onKeyConfigChanged(const KeyConfig &config)
Expand All @@ -485,6 +530,12 @@
}
} else { // exist
KeyConfig &old = m_keyConfigsMap[config.getId()];

// No actual change, skip to avoid spurious ShortcutChanged signal
if (old == config) {
return;
}

if (!config.enabled) {
// enable->disable
m_keyHandler->unregisterKey(config.getId());
Expand Down Expand Up @@ -548,6 +599,12 @@
return false;
}

if (config.hotkeys.isEmpty()) {
qInfo() << "Shortcut has no hotkeys, keeping it visible without registration:"
<< config.getId();
return true;
}

if (m_keyConfigsMap.contains(config.getId())) {
qWarning() << "Shortcut conflict detected during init: has same appId and displayName"
<< "hotkeys:" << config.hotkeys
Expand Down Expand Up @@ -647,14 +704,24 @@
// Emit hotkeys in X11/XKB form (e.g. "<Control><Alt>T", "XF86AudioMute")
// so the legacy control-center shortcut page can render each modifier as
// a separate chip without doing its own format translation.
info.hotkeys.reserve(config.hotkeys.size());
for (const QString &hk : config.hotkeys) {
info.hotkeys.append(QKeySequenceConverter::qKeySequenceToXkb(hk));
if (config.hotkeys.isEmpty()) {
info.hotkeys.append(localizedNoHotkeyText());
} else {
info.hotkeys.reserve(config.hotkeys.size());
for (const QString &hk : config.hotkeys) {
info.hotkeys.append(QKeySequenceConverter::qKeySequenceToXkb(hk));
}
}
info.localLanguageName = m_translationManager->translate(config.appId, config.displayName);
return info;
}

QString KeybindingManager::localizedNoHotkeyText() const
{
return m_translationManager->translate(QString::fromLatin1(kDefaultShortcutAppId),
QString::fromLatin1(kNoHotkeyText));
}

uint KeybindingManager::GetNumLockState() const
{
if (m_keyHandler) {
Expand Down
2 changes: 1 addition & 1 deletion src/plugin-qt/shortcut/src/core/keybindingmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ private slots:
void rollbackRegistration(const QString &id1, const QString &id2,
KeyConfig &config1, KeyConfig &config2,
const QStringList &hotkeys1, const QStringList &hotkeys2);
QString localizedNoHotkeyText() const;


ConfigLoader *m_loader;
Expand Down Expand Up @@ -131,4 +132,3 @@ inline const QDBusArgument &operator>>(const QDBusArgument &argument, ShortcutIn
return argument;
}


19 changes: 18 additions & 1 deletion src/plugin-qt/shortcut/src/core/shortcutconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,26 @@
QStringList hotkeys; // List of key combinations
int keyEventFlags; // KeyEventFlag bitfield: Press=0x1, Release=0x2, Repeat=0x4

KeyConfig() : keyEventFlags(KeyEventFlag::Release) {} // Default to release

Check warning on line 51 in src/plugin-qt/shortcut/src/core/shortcutconfig.h

View workflow job for this annotation

GitHub Actions / cppcheck

Member variable 'BaseConfig::triggerType' is not initialized in the constructor. Maybe it should be initialized directly in the class BaseConfig? Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior.

Check warning on line 51 in src/plugin-qt/shortcut/src/core/shortcutconfig.h

View workflow job for this annotation

GitHub Actions / cppcheck

Member variable 'BaseConfig::modifiable' is not initialized in the constructor. Maybe it should be initialized directly in the class BaseConfig? Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior.

Check warning on line 51 in src/plugin-qt/shortcut/src/core/shortcutconfig.h

View workflow job for this annotation

GitHub Actions / cppcheck

Member variable 'BaseConfig::enabled' is not initialized in the constructor. Maybe it should be initialized directly in the class BaseConfig? Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior.

Check warning on line 51 in src/plugin-qt/shortcut/src/core/shortcutconfig.h

View workflow job for this annotation

GitHub Actions / cppcheck

Member variable 'BaseConfig::category' is not initialized in the constructor. Maybe it should be initialized directly in the class BaseConfig? Member variables of native types, pointers, or references are left uninitialized when the class is instantiated. That may cause bugs or undefined behavior.

bool isValid() const { return enabled && !appId.isEmpty() && !displayName.isEmpty() && !hotkeys.isEmpty(); }
bool isValid() const { return enabled && !appId.isEmpty() && !displayName.isEmpty(); }

bool operator==(const KeyConfig &other) const {
return appId == other.appId &&
subPath == other.subPath &&
displayName == other.displayName &&
category == other.category &&
enabled == other.enabled &&
modifiable == other.modifiable &&
triggerType == other.triggerType &&
triggerValue == other.triggerValue &&
hotkeys == other.hotkeys &&
keyEventFlags == other.keyEventFlags;
}

bool operator!=(const KeyConfig &other) const {
return !(*this == other);
}
};

enum class GestureType {
Expand Down
3 changes: 2 additions & 1 deletion src/plugin-qt/shortcut/tools/extract_shortcuts_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def extract_translations(config_dir, output_file, app_id):
Scans the config_dir for org.deepin.shortcut.json / org.deepin.gesture.json
files and extracts the displayName field into a dummy C++ file for lupdate scanning.
"""
display_names = set()
# Fixed display text used when a shortcut has no assigned key sequence.
display_names = {"None"}
target_files = {"org.deepin.shortcut.json", "org.deepin.gesture.json"}

for root, dirs, files in os.walk(config_dir):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
<source>Default terminal</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>None</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
<source>Default terminal</source>
<translation>终端</translation>
</message>
<message>
<source>None</source>
<translation>无</translation>
</message>
</context>
</TS>
Loading