You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
settings/src/shortcuts/shortcutsmodel.cpp

487 lines
21 KiB
C++

#include "shortcutsmodel.h"
#include <QDBusReply>
#include <QKeySequence>
#include <QDBusServiceWatcher>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
static constexpr auto service = "com.cutefish.Shortcuts1";
static constexpr auto path = "/com/cutefish/Shortcuts1";
namespace {
// Qt's key codes for the keys that are not printable characters, and the xkb
// keysyms the compositor knows them by. Only used when the toolkit reports no
// scan code; the compositor resolves the scan code itself otherwise.
struct QtKeysym {
int qtKey;
int keysym;
};
} // namespace
static const QtKeysym kQtKeysyms[] = {
{Qt::Key_Escape, 0xff1b}, {Qt::Key_Tab, 0xff09}, {Qt::Key_Backtab, 0xfe20},
{Qt::Key_Backspace, 0xff08}, {Qt::Key_Return, 0xff0d}, {Qt::Key_Enter, 0xff8d},
{Qt::Key_Insert, 0xff63}, {Qt::Key_Delete, 0xffff}, {Qt::Key_Pause, 0xff13},
{Qt::Key_Print, 0xff61}, {Qt::Key_Home, 0xff50}, {Qt::Key_End, 0xff57},
{Qt::Key_Left, 0xff51}, {Qt::Key_Up, 0xff52}, {Qt::Key_Right, 0xff53},
{Qt::Key_Down, 0xff54}, {Qt::Key_PageUp, 0xff55}, {Qt::Key_PageDown, 0xff56},
{Qt::Key_Space, 0x20}, {Qt::Key_Menu, 0xff67}, {Qt::Key_Meta, 0xffeb},
{Qt::Key_Super_L, 0xffeb}, {Qt::Key_Super_R, 0xffec},
{Qt::Key_VolumeDown, 0x1008ff11}, {Qt::Key_VolumeMute, 0x1008ff12},
{Qt::Key_VolumeUp, 0x1008ff13}, {Qt::Key_MediaPlay, 0x1008ff14},
{Qt::Key_MediaStop, 0x1008ff15}, {Qt::Key_MediaPrevious, 0x1008ff16},
{Qt::Key_MediaNext, 0x1008ff17}, {Qt::Key_MonBrightnessUp, 0x1008ff02},
{Qt::Key_MonBrightnessDown, 0x1008ff03}, {Qt::Key_Calculator, 0x1008ff1d},
};
ShortcutsModel::ShortcutsModel(QObject *parent)
: QAbstractListModel(parent)
{
auto *watcher = new QDBusServiceWatcher(QLatin1String(service), QDBusConnection::sessionBus(),
QDBusServiceWatcher::WatchForOwnerChange, this);
connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this,
&ShortcutsModel::refreshInterface);
refreshInterface();
}
ShortcutsModel::~ShortcutsModel()
{
setRecording(false);
}
QStringList ShortcutsModel::categories() const
{
return {tr("General"), tr("Windows")};
}
void ShortcutsModel::refreshInterface()
{
delete m_interface;
m_interface = new QDBusInterface(QLatin1String(service), QLatin1String(path),
QLatin1String(service), QDBusConnection::sessionBus(), this);
const bool available = m_interface->isValid();
if (available)
connect(m_interface, SIGNAL(acceleratorsChanged()), this, SLOT(reload()));
if (available != m_available) {
m_available = available;
emit availableChanged();
}
reload();
}
void ShortcutsModel::reload()
{
// The order here is the order on the page, like kAcceleratorLayouts.
const QString general = tr("General");
const QString windows = tr("Windows");
const QString desks = tr("Desks");
const QString screenshots = tr("Screenshots and Recording");
const Layout layouts[] = {
{"toggle-app-list", general, tr("Open the launcher")},
{"lock-screen", general, tr("Lock the screen")},
{"cycle-forward-mru", windows, tr("Switch to the next window")},
{"cycle-backward-mru", windows, tr("Switch to the previous window")},
{"toggle-overview", windows, tr("Show all windows")},
{"toggle-show-desktop", windows, tr("Show the desktop")},
{"toggle-maximized", windows, tr("Maximize or restore the window")},
{"window-minimize", windows, tr("Minimize the window")},
{"quit-application", windows, tr("Quit the application")},
{"hide-application", windows, tr("Hide the application")},
{"hide-other-applications", windows, tr("Hide other applications")},
{"toggle-fullscreen", windows, tr("Toggle full screen")},
{"window-cycle-snap-left", windows, tr("Snap the window to the left")},
{"window-cycle-snap-right", windows, tr("Snap the window to the right")},
{"close-window", windows, tr("Close the window")},
{"move-active-window-between-displays", windows, tr("Move the window to the next display")},
{"desks-activate-desk-left", desks, tr("Switch to the desk on the left")},
{"desks-activate-desk-right", desks, tr("Switch to the desk on the right")},
{"desks-move-active-item-left", desks, tr("Move the window to the desk on the left")},
{"desks-move-active-item-right", desks, tr("Move the window to the desk on the right")},
{"desks-new-desk", desks, tr("Add a desk")},
{"desks-remove-current-desk", desks, tr("Remove the desk")},
{"desks-activate-0", desks, tr("Switch to desk 1")},
{"desks-activate-1", desks, tr("Switch to desk 2")},
{"desks-activate-2", desks, tr("Switch to desk 3")},
{"desks-activate-3", desks, tr("Switch to desk 4")},
{"desks-activate-4", desks, tr("Switch to desk 5")},
{"desks-activate-5", desks, tr("Switch to desk 6")},
{"desks-activate-6", desks, tr("Switch to desk 7")},
{"desks-activate-7", desks, tr("Switch to desk 8")},
{"desks-toggle-assign-to-all-desks", desks, tr("Show the window on every desk")},
{"take-screenshot", screenshots, tr("Save a picture of the screen")},
{"take-screenshot-to-clipboard", screenshots, tr("Copy a picture of the screen")},
{"take-partial-screenshot", screenshots, tr("Save a picture of the selected area")},
{"take-partial-screenshot-to-clipboard", screenshots,
tr("Copy a picture of the selected area")},
{"start-capture-mode", screenshots, tr("Screenshot and recording options")},
{"stop-screen-recording", screenshots, tr("Stop screen recording")},
};
QHash<QString, QJsonObject> published;
bool hasCustom = false;
if (m_interface && m_interface->isValid()) {
const QDBusReply<QString> reply = m_interface->call(QStringLiteral("accelerators"));
if (reply.isValid()) {
const QJsonArray actions = QJsonDocument::fromJson(reply.value().toUtf8())
.object()
.value(QStringLiteral("actions"))
.toArray();
for (const QJsonValue &value : actions) {
const QJsonObject action = value.toObject();
published.insert(action.value(QStringLiteral("name")).toString(), action);
}
}
const QDBusReply<bool> custom = m_interface->call(QStringLiteral("hasCustomAccelerators"));
hasCustom = custom.isValid() && custom.value();
m_commandKeys = m_interface->property("shortcutConvention").toString()
== QLatin1String("command");
}
beginResetModel();
m_actions.clear();
for (const Layout &layout : layouts) {
const auto it = published.constFind(QLatin1String(layout.name));
// An action the compositor does not publish is one it cannot perform.
if (it == published.constEnd())
continue;
Action action;
action.name = QLatin1String(layout.name);
action.category = layout.category;
action.description = layout.description;
for (const QJsonValue &value : it->value(QStringLiteral("accelerators")).toArray())
action.accelerators.append(acceleratorFromJson(value.toObject()));
for (const QJsonValue &value : it->value(QStringLiteral("defaults")).toArray())
action.defaults.append(acceleratorFromJson(value.toObject()));
m_actions.append(action);
}
endResetModel();
m_hasCustomAccelerators = hasCustom;
emit acceleratorsChanged();
}
QVariantMap ShortcutsModel::acceleratorFromJson(const QJsonObject &object) const
{
QVariantMap accelerator;
const int modifiers = object.value(QStringLiteral("modifiers")).toInt();
const QString keyName = object.value(QStringLiteral("keyName")).toString();
const QString keyText = object.value(QStringLiteral("keyText")).toString();
accelerator.insert(QStringLiteral("keysym"), object.value(QStringLiteral("keysym")).toInt());
accelerator.insert(QStringLiteral("modifiers"), modifiers);
accelerator.insert(QStringLiteral("keyName"), keyName);
accelerator.insert(QStringLiteral("type"), object.value(QStringLiteral("type")).toString());
accelerator.insert(QStringLiteral("locked"), object.value(QStringLiteral("locked")).toBool());
QStringList parts = modifierParts(modifiers);
parts.append(keyDisplay(keyName, keyText));
accelerator.insert(QStringLiteral("parts"), parts);
accelerator.insert(QStringLiteral("text"), parts.join(QStringLiteral(" + ")));
return accelerator;
}
QString ShortcutsModel::keyDisplay(const QString &keyName, const QString &keyText) const
{
if (keyName == QLatin1String("Super_L") || keyName == QLatin1String("Super_R"))
return m_commandKeys ? tr("Command") : tr("Meta");
static const QHash<QString, QString> names = {
{QStringLiteral("Up"), QStringLiteral("↑")},
{QStringLiteral("Down"), QStringLiteral("↓")},
{QStringLiteral("Left"), QStringLiteral("←")},
{QStringLiteral("Right"), QStringLiteral("→")},
{QStringLiteral("Escape"), tr("Esc")},
{QStringLiteral("Return"), tr("Enter")},
{QStringLiteral("KP_Enter"), tr("Enter")},
{QStringLiteral("space"), tr("Space")},
{QStringLiteral("BackSpace"), tr("Backspace")},
{QStringLiteral("Delete"), tr("Delete")},
{QStringLiteral("Insert"), tr("Insert")},
{QStringLiteral("Home"), tr("Home")},
{QStringLiteral("End"), tr("End")},
{QStringLiteral("Prior"), tr("Page Up")},
{QStringLiteral("Next"), tr("Page Down")},
{QStringLiteral("Print"), tr("Print Screen")},
{QStringLiteral("Tab"), tr("Tab")},
{QStringLiteral("Menu"), tr("Menu")},
{QStringLiteral("XF86AudioRaiseVolume"), tr("Volume Up")},
{QStringLiteral("XF86AudioLowerVolume"), tr("Volume Down")},
{QStringLiteral("XF86AudioMute"), tr("Mute")},
{QStringLiteral("XF86MonBrightnessUp"), tr("Brightness Up")},
{QStringLiteral("XF86MonBrightnessDown"), tr("Brightness Down")},
{QStringLiteral("XF86AudioPlay"), tr("Play/Pause")},
{QStringLiteral("XF86AudioPause"), tr("Pause")},
{QStringLiteral("XF86AudioNext"), tr("Next Track")},
{QStringLiteral("XF86AudioPrev"), tr("Previous Track")},
{QStringLiteral("XF86AudioStop"), tr("Stop")},
{QStringLiteral("XF86Calculator"), tr("Calculator")},
{QStringLiteral("XF86Explorer"), tr("File Manager")},
{QStringLiteral("XF86Search"), tr("Search")},
{QStringLiteral("XF86HomePage"), tr("Home Page")},
{QStringLiteral("XF86Mail"), tr("Mail")},
};
if (!keyText.isEmpty())
return keyText;
const auto it = names.constFind(keyName);
if (it != names.constEnd())
return it.value();
return keyName;
}
int ShortcutsModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_actions.size();
}
QVariant ShortcutsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_actions.size())
return QVariant();
const Action &action = m_actions.at(index.row());
switch (role) {
case NameRole:
return action.name;
case DescriptionRole:
return action.description;
case CategoryRole:
return action.category;
case AcceleratorsRole:
return action.accelerators;
case IsDefaultRole:
return action.accelerators == action.defaults;
}
return QVariant();
}
QHash<int, QByteArray> ShortcutsModel::roleNames() const
{
return {
{NameRole, "name"},
{DescriptionRole, "description"},
{CategoryRole, "category"},
{AcceleratorsRole, "accelerators"},
{IsDefaultRole, "isDefault"},
};
}
QVariantMap ShortcutsModel::call(const QString &method, const QVariantList &arguments)
{
if (!m_interface || !m_interface->isValid()) {
return {{QStringLiteral("result"), QStringLiteral("unavailable")},
{QStringLiteral("message"), tr("The shortcut service is not available.")}};
}
const QDBusReply<QString> reply = m_interface->callWithArgumentList(QDBus::Block, method,
arguments);
if (!reply.isValid()) {
qWarning() << "Shortcut request" << method << "failed:" << reply.error().message();
return {{QStringLiteral("result"), QStringLiteral("error")},
{QStringLiteral("message"), reply.error().message()}};
}
return resultFromJson(reply.value());
}
QVariantMap ShortcutsModel::resultFromJson(const QString &json) const
{
const QJsonObject object = QJsonDocument::fromJson(json.toUtf8()).object();
const QString result = object.value(QStringLiteral("result")).toString();
const QString conflictAction = object.value(QStringLiteral("conflictAction")).toString();
const QString conflictDescription = descriptionFor(conflictAction);
return {
{QStringLiteral("result"), result},
{QStringLiteral("conflictAction"), conflictAction},
{QStringLiteral("conflictDescription"), conflictDescription},
{QStringLiteral("message"), messageFor(result, conflictDescription)},
};
}
QString ShortcutsModel::messageFor(const QString &result, const QString &conflictDescription) const
{
if (result == QLatin1String("success"))
return QString();
if (result == QLatin1String("conflict"))
return tr("This shortcut is already used by \"%1\".").arg(conflictDescription);
if (result == QLatin1String("conflict-can-override"))
return tr("This shortcut is used by \"%1\". Press it again to reassign it.")
.arg(conflictDescription);
if (result == QLatin1String("missing-modifier")) {
return m_commandKeys
? tr("Include Control, Option or Command, or use a function key.")
: tr("Include Ctrl, Alt or Meta, or use a function key.");
}
if (result == QLatin1String("shift-only-not-allowed")) {
return m_commandKeys
? tr("Shift cannot be the only modifier. Add Control, Option or Command.")
: tr("Shift cannot be the only modifier. Add Ctrl, Alt or Meta.");
}
if (result == QLatin1String("key-not-allowed") || result == QLatin1String("reserved-key-not-allowed"))
return tr("This key cannot be used in a shortcut.");
if (result == QLatin1String("maximum-accelerators-reached"))
return tr("This action already has the maximum of five shortcuts.");
if (result == QLatin1String("non-meta-accelerator-warning")) {
return m_commandKeys ? tr("Shortcuts without both Control and Command may be intercepted "
"before applications see them. Press it again to keep it.")
: tr("Shortcuts without Meta may be intercepted before applications "
"see them. Press it again to keep it.");
}
if (result == QLatin1String("restore-success-with-conflicts"))
return tr("Some default shortcuts are in use by other actions and were not restored.");
if (result == QLatin1String("action-locked") || result == QLatin1String("accelerator-locked"))
return tr("This shortcut cannot be changed.");
if (result == QLatin1String("not-found"))
return tr("This shortcut no longer exists.");
return tr("The shortcut could not be changed.");
}
QVariantMap ShortcutsModel::addAccelerator(const QString &action, int keycode, int keysym,
int modifiers)
{
return call(QStringLiteral("addAccelerator"),
{action, static_cast<uint>(qMax(0, keycode)), static_cast<uint>(qMax(0, keysym)),
static_cast<uint>(modifiers)});
}
QVariantMap ShortcutsModel::replaceAccelerator(const QString &action, int oldKeysym,
int oldModifiers, int keycode, int keysym,
int modifiers)
{
return call(QStringLiteral("replaceAccelerator"),
{action, static_cast<uint>(oldKeysym), static_cast<uint>(oldModifiers),
static_cast<uint>(qMax(0, keycode)), static_cast<uint>(qMax(0, keysym)),
static_cast<uint>(modifiers)});
}
QVariantMap ShortcutsModel::removeAccelerator(const QString &action, int keysym, int modifiers)
{
return call(QStringLiteral("removeAccelerator"),
{action, static_cast<uint>(keysym), static_cast<uint>(modifiers)});
}
QVariantMap ShortcutsModel::restoreDefault(const QString &action)
{
return call(QStringLiteral("restoreDefault"), {action});
}
QVariantMap ShortcutsModel::restoreAllDefaults()
{
return call(QStringLiteral("restoreAllDefaults"), {});
}
void ShortcutsModel::setRecording(bool recording)
{
if (m_interface && m_interface->isValid())
m_interface->asyncCall(QStringLiteral("preventProcessingAccelerators"), recording);
}
QStringList ShortcutsModel::modifierParts(int modifiers) const
{
// Under the Command convention Meta is the Command key next to the space
// bar, Alt is Option and Ctrl is Control.
QStringList parts;
if (modifiers & ControlDown)
parts << (m_commandKeys ? tr("Control") : tr("Ctrl"));
if (modifiers & AltDown)
parts << (m_commandKeys ? tr("Option") : tr("Alt"));
if (modifiers & ShiftDown)
parts << tr("Shift");
if (modifiers & MetaDown)
parts << (m_commandKeys ? tr("Command") : tr("Meta"));
return parts;
}
QStringList ShortcutsModel::partsForKey(int keycode, int qtKey, int modifiers)
{
QStringList parts = modifierParts(modifiers);
if (isModifierKey(qtKey))
return parts;
QString display;
if (m_interface && m_interface->isValid()) {
const QDBusReply<QString> reply =
m_interface->call(QStringLiteral("describeKey"), static_cast<uint>(qMax(0, keycode)),
static_cast<uint>(keysymForQtKey(qtKey)));
if (reply.isValid()) {
const QJsonObject object = QJsonDocument::fromJson(reply.value().toUtf8()).object();
display = keyDisplay(object.value(QStringLiteral("keyName")).toString(),
object.value(QStringLiteral("keyText")).toString());
}
}
if (display.isEmpty())
display = QKeySequence(qtKey).toString(QKeySequence::NativeText);
parts << display;
return parts;
}
bool ShortcutsModel::isModifierKey(int qtKey) const
{
switch (qtKey) {
case Qt::Key_Shift:
case Qt::Key_Control:
case Qt::Key_Alt:
case Qt::Key_AltGr:
case Qt::Key_Meta:
case Qt::Key_Super_L:
case Qt::Key_Super_R:
case Qt::Key_Hyper_L:
case Qt::Key_Hyper_R:
case Qt::Key_CapsLock:
case Qt::Key_NumLock:
case Qt::Key_ScrollLock:
case Qt::Key_unknown:
return true;
default:
return false;
}
}
int ShortcutsModel::compositorModifiers(int qtModifiers) const
{
int modifiers = 0;
if (qtModifiers & Qt::ShiftModifier)
modifiers |= ShiftDown;
if (qtModifiers & Qt::ControlModifier)
modifiers |= ControlDown;
if (qtModifiers & Qt::AltModifier)
modifiers |= AltDown;
if (qtModifiers & Qt::MetaModifier)
modifiers |= MetaDown;
return modifiers;
}
int ShortcutsModel::keysymForQtKey(int qtKey) const
{
for (const QtKeysym &entry : kQtKeysyms) {
if (entry.qtKey == qtKey)
return entry.keysym;
}
if (qtKey >= Qt::Key_F1 && qtKey <= Qt::Key_F35)
return 0xffbe + (qtKey - Qt::Key_F1);
// Printable Latin-1 keys are their character, upper-cased by Qt; the
// compositor lower-cases them.
if (qtKey >= 0x20 && qtKey <= 0xff)
return qtKey;
return 0;
}
QVariantMap ShortcutsModel::action(const QString &name) const
{
for (const Action &candidate : m_actions) {
if (candidate.name != name)
continue;
return {{QStringLiteral("description"), candidate.description},
{QStringLiteral("accelerators"), candidate.accelerators},
{QStringLiteral("isDefault"), candidate.accelerators == candidate.defaults}};
}
return {};
}
QString ShortcutsModel::descriptionFor(const QString &action) const
{
for (const Action &candidate : m_actions) {
if (candidate.name == action)
return candidate.description;
}
return action;
}