kdelibs/kdeui/shortcuts/kglobalaccel.cpp
Ivailo Monev 6784f51d58 kdeui: tweak KNotificationManager and KGlobalAccel for automatic service activation
KGlobalAccelPrivate uses service watcher and the interface is invalid until
the service is activated (either by the first call of the interface or some
other lazy-initialization method), however KNotificationManager does
connect to signals immediately (which should work regardless) but the error
may have been logged before

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2022-12-09 12:00:23 +02:00

503 lines
16 KiB
C++

/* This file is part of the KDE libraries
Copyright (C) 2001,2002 Ellis Whitehead <ellis@kde.org>
Copyright (C) 2006 Hamish Rodda <rodda@kde.org>
Copyright (C) 2007 Andreas Hartmetz <ahartmetz@gmail.com>
Copyright (C) 2008 Michael Jansen <kde@michael-jansen.biz>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "kglobalaccel.h"
#include "kglobalaccel_p.h"
#include <memory>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusMetaType>
#ifdef Q_WS_X11
#include <QtGui/qx11info_x11.h>
#include <netwm_def.h>
#endif
#include <kdebug.h>
#include <ktoolinvocation.h>
#include <kaboutdata.h>
#include <kcomponentdata.h>
#include "kaction.h"
#include "kaction_p.h"
#include "kmessagebox.h"
#include "kshortcut.h"
org::kde::kglobalaccel::Component *KGlobalAccelPrivate::getComponent(const QString &componentUnique, bool remember)
{
// Check if we already have this component
if (components.contains(componentUnique)) {
return components[componentUnique];
}
// Get the path for our component. We have to do that because
// componentUnique is probably not a valid dbus object path
QDBusReply<QDBusObjectPath> reply = iface.getComponent(componentUnique);
if (!reply.isValid()) {
if (reply.error().name() == "org.kde.kglobalaccel.NoSuchComponent") {
// No problem. The component doesn't exists. That's normal
return NULL;
}
// An unknown error.
kError() << "Failed to get dbus path for component " << componentUnique << reply.error();
return NULL;
}
// Now get the component
org::kde::kglobalaccel::Component *component = new org::kde::kglobalaccel::Component(
"org.kde.kglobalaccel",
reply.value().path(),
QDBusConnection::sessionBus(),
q);
// No component no cleaning
if (!component->isValid()) {
kDebug() << "Failed to get component" << componentUnique << QDBusConnection::sessionBus().lastError();
return NULL;
}
if (remember)
{
// Connect to the signals we are interested in.
q->connect(component, SIGNAL(globalShortcutPressed(QString,QString,qlonglong)),
SLOT(_k_invokeAction(QString,QString,qlonglong)));
components[componentUnique] = component;
}
return component;
}
KGlobalAccelPrivate::KGlobalAccelPrivate(KGlobalAccel *q)
: iface("org.kde.kglobalaccel", "/kglobalaccel", QDBusConnection::sessionBus()),
q(q)
{
QDBusServiceWatcher *watcher = new QDBusServiceWatcher(iface.service(),
QDBusConnection::sessionBus(),
QDBusServiceWatcher::WatchForOwnerChange,
q);
q->connect(watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)),
q, SLOT(_k_serviceOwnerChanged(QString,QString,QString)));
}
void KGlobalAccelPrivate::readComponentData(const KComponentData &componentData)
{
Q_ASSERT(!componentData.componentName().isEmpty());
mainComponent = componentData;
if (componentData.aboutData()->programName().isEmpty()) {
kDebug(125) << componentData.componentName() << " has empty programName()";
}
}
KGlobalAccel::KGlobalAccel()
: d(new KGlobalAccelPrivate(this))
{
qDBusRegisterMetaType<QList<int> >();
qDBusRegisterMetaType<QList<QStringList> >();
qDBusRegisterMetaType<KGlobalShortcutInfo>();
qDBusRegisterMetaType<QList<KGlobalShortcutInfo> >();
connect(&d->iface, SIGNAL(yourShortcutGotChanged(QStringList,QList<int>)),
SLOT(_k_shortcutGotChanged(QStringList,QList<int>)));
if (KGlobal::hasMainComponent()) {
d->readComponentData( KGlobal::mainComponent() );
}
}
KGlobalAccel::~KGlobalAccel()
{
delete d;
}
void KGlobalAccel::activateGlobalShortcutContext(
const QString &contextUnique,
const QString &contextFriendly,
const KComponentData &component)
{
Q_UNUSED(contextFriendly);
// TODO: provide contextFriendly
self()->d->iface.activateGlobalShortcutContext(component.aboutData()->programName(), contextUnique);
}
// static
bool KGlobalAccel::cleanComponent(const QString &componentUnique)
{
org::kde::kglobalaccel::Component* component = self()->getComponent(componentUnique);
if (!component) return false;
return component->cleanUp();
}
// static
bool KGlobalAccel::isComponentActive(const QString &componentUnique)
{
org::kde::kglobalaccel::Component* component = self()->getComponent(componentUnique);
if (!component) return false;
return component->isActive();
}
org::kde::kglobalaccel::Component *KGlobalAccel::getComponent(const QString &componentUnique)
{
return d->getComponent(componentUnique);
}
KGlobalAccel *KGlobalAccel::self()
{
K_GLOBAL_STATIC(KGlobalAccel, s_instance)
return s_instance;
}
void KGlobalAccelPrivate::doRegister(KAction *action)
{
if (!action || action->objectName().isEmpty()) {
return;
}
const bool isRegistered = actions.contains(action);
if (isRegistered) {
return;
}
QStringList actionId = makeActionId(action);
nameToAction.insertMulti(actionId.at(KGlobalAccel::ActionUnique), action);
actions.insert(action);
iface.doRegister(actionId);
}
void KGlobalAccelPrivate::remove(KAction *action, Removal removal)
{
if (!action || action->objectName().isEmpty()) {
return;
}
const bool isRegistered = actions.contains(action);
if (!isRegistered) {
return;
}
QStringList actionId = makeActionId(action);
nameToAction.remove(actionId.at(KGlobalAccel::ActionUnique), action);
actions.remove(action);
if (removal == UnRegister) {
// Complete removal of the shortcut is requested
// (forgetGlobalShortcut)
iface.unregister(actionId.at(KGlobalAccel::ComponentUnique), actionId.at(KGlobalAccel::ActionUnique));
} else {
// If the action is a configurationAction wen only remove it from our
// internal registry. That happened above.
if (!action->property("isConfigurationAction").toBool()) {
// If it's a session shortcut unregister it.
if (action->objectName().startsWith(QLatin1String("_k_session:"))) {
iface.unregister(actionId.at(KGlobalAccel::ComponentUnique), actionId.at(KGlobalAccel::ActionUnique));
} else {
iface.setInactive(actionId);
}
}
}
}
void KGlobalAccelPrivate::updateGlobalShortcut(KAction *action, uint flags)
{
// No action or no objectname -> Do nothing
// KAction::setGlobalShortcut informs the user
if (!action || action->objectName().isEmpty()) {
return;
}
QStringList actionId = makeActionId(action);
const KShortcut activeShortcut = action->globalShortcut();
const KShortcut defaultShortcut = action->globalShortcut(KAction::DefaultShortcut);
uint setterFlags = 0;
if (flags & KAction::NoAutoloading) {
setterFlags |= NoAutoloading;
}
if (flags & KAction::ActiveShortcut) {
bool isConfigurationAction = action->property("isConfigurationAction").toBool();
uint activeSetterFlags = setterFlags;
// setPresent tells kglobalaccel that the shortcut is active
if (!isConfigurationAction) {
activeSetterFlags |= SetPresent;
}
// Sets the shortcut, returns the active/real keys
const QList<int> result = iface.setShortcut(
actionId,
intListFromShortcut(activeShortcut),
activeSetterFlags);
// Make sure we get informed about changes in the component by kglobalaccel
getComponent(componentUniqueForAction(action), true);
// Create a shortcut from the result
const KShortcut scResult(shortcutFromIntList(result));
if (isConfigurationAction && (flags & KAction::NoAutoloading)) {
// If this is a configuration action and we have set the shortcut,
// inform the real owner of the change.
// Note that setForeignShortcut will cause a signal to be sent to applications
// even if it did not "see" that the shortcut has changed. This is Good because
// at the time of comparison (now) the action *already has* the new shortcut.
// We called setShortcut(), remember?
// Also note that we will see our own signal so we may not need to call
// setActiveGlobalShortcutNoEnable - _k_shortcutGotChanged() does it.
// In practice it's probably better to get the change propagated here without
// DBus delay as we do below.
iface.setForeignShortcut(actionId, result);
}
if (scResult != activeShortcut) {
// If kglobalaccel returned a shortcut that differs from the one we
// sent, use that one. There must have been clashes or some other problem.
action->d->setActiveGlobalShortcutNoEnable(scResult);
}
}
if (flags & KAction::DefaultShortcut) {
iface.setShortcut(actionId, intListFromShortcut(defaultShortcut),
setterFlags | IsDefault);
}
}
QStringList KGlobalAccelPrivate::makeActionId(const KAction *action)
{
QStringList ret(componentUniqueForAction(action)); // Component Unique Id ( see actionIdFields )
Q_ASSERT(!ret.at(KGlobalAccel::ComponentUnique).isEmpty());
Q_ASSERT(!action->objectName().isEmpty());
ret.append(action->objectName()); // Action Unique Name
ret.append(componentFriendlyForAction(action)); // Component Friendly name
const QString actionText = KGlobal::locale()->removeAcceleratorMarker(action->text());
ret.append(actionText); // Action Friendly Name
return ret;
}
QList<int> KGlobalAccelPrivate::intListFromShortcut(const KShortcut &cut)
{
QList<int> ret;
ret.append(cut.primary()[0]);
ret.append(cut.alternate()[0]);
while (!ret.isEmpty() && ret.last() == 0)
ret.removeLast();
return ret;
}
KShortcut KGlobalAccelPrivate::shortcutFromIntList(const QList<int> &list)
{
KShortcut ret;
if (list.count() > 0)
ret.setPrimary(list[0]);
if (list.count() > 1)
ret.setAlternate(list[1]);
return ret;
}
QString KGlobalAccelPrivate::componentUniqueForAction(const KAction *action)
{
Q_ASSERT(action->d->componentData.isValid());
return action->d->componentData.componentName();
}
QString KGlobalAccelPrivate::componentFriendlyForAction(const KAction *action)
{
Q_ASSERT(action->d->componentData.isValid());
return action->d->componentData.aboutData()->programName();
}
void KGlobalAccelPrivate::_k_invokeAction(
const QString &componentUnique,
const QString &actionUnique,
qlonglong timestamp)
{
KAction *action = 0;
QList<KAction *> candidates = nameToAction.values(actionUnique);
foreach (KAction *const a, candidates) {
if (componentUniqueForAction(a) == componentUnique) {
action = a;
}
}
// We do not trigger if
// - there is no action
// - the action is not enabled
// - the action is an configuration action
if (!action || !action->isEnabled() || action->property("isConfigurationAction").toBool()) {
return;
}
#ifdef Q_WS_X11
// Update this application's X timestamp if needed.
// TODO The 100%-correct solution should probably be handling this action
// in the proper place in relation to the X events queue in order to avoid
// the possibility of wrong ordering of user events.
if( NET::timestampCompare(timestamp, QX11Info::appTime()) > 0)
QX11Info::setAppTime(timestamp);
if( NET::timestampCompare(timestamp, QX11Info::appUserTime()) > 0)
QX11Info::setAppUserTime(timestamp);
#else
Q_UNUSED(timestamp);
#endif
action->trigger();
}
void KGlobalAccelPrivate::_k_shortcutGotChanged(const QStringList &actionId,
const QList<int> &keys)
{
KAction *action = nameToAction.value(actionId.at(KGlobalAccel::ActionUnique));
if (!action)
return;
action->d->setActiveGlobalShortcutNoEnable(shortcutFromIntList(keys));
}
void KGlobalAccelPrivate::_k_serviceOwnerChanged(const QString &name, const QString &oldOwner,
const QString &newOwner)
{
Q_UNUSED(oldOwner);
if (name == QLatin1String("org.kde.kglobalaccel") && !newOwner.isEmpty()) {
// kglobalaccel was restarted
kDebug(125) << "detected kglobalaccel restarting, re-registering all shortcut keys";
reRegisterAll();
}
}
void KGlobalAccelPrivate::reRegisterAll()
{
//We clear all our data, assume that all data on the other side is clear too,
//and register each action as if it just was allowed to have global shortcuts.
//If the kded side still has the data it doesn't matter because of the
//autoloading mechanism. The worst case I can imagine is that an action's
//shortcut was changed but the kded side died before it got the message so
//autoloading will now assign an old shortcut to the action. Particularly
//picky apps might assert or misbehave.
QSet<KAction *> allActions = actions;
nameToAction.clear();
actions.clear();
foreach(KAction *const action, allActions) {
doRegister(action);
updateGlobalShortcut(action, KAction::Autoloading | KAction::ActiveShortcut);
}
}
QList<KGlobalShortcutInfo> KGlobalAccel::getGlobalShortcutsByKey(const QKeySequence &seq)
{
return self()->d->iface.getGlobalShortcutsByKey(seq[0]);
}
bool KGlobalAccel::isGlobalShortcutAvailable(const QKeySequence &seq, const QString &comp)
{
return self()->d->iface.isGlobalShortcutAvailable(seq[0], comp);
}
//static
bool KGlobalAccel::promptStealShortcutSystemwide(
QWidget *parent,
const QList<KGlobalShortcutInfo> &shortcuts,
const QKeySequence &seq)
{
if (shortcuts.isEmpty()) {
// Usage error. Just say no
return false;
}
QString component = shortcuts[0].componentFriendlyName();
QString message;
if (shortcuts.size()==1) {
message = i18n("The '%1' key combination is registered by application %2 for action %3:",
seq.toString(),
component,
shortcuts[0].friendlyName());
} else {
QString actionList;
Q_FOREACH(const KGlobalShortcutInfo &info, shortcuts) {
actionList += i18n("In context '%1' for action '%2'\n",
info.contextFriendlyName(),
info.friendlyName());
}
message = i18n("The '%1' key combination is registered by application %2.\n%3",
seq.toString(),
component,
actionList);
}
QString title = i18n("Conflict With Registered Global Shortcut");
return KMessageBox::warningContinueCancel(parent, message, title, KGuiItem(i18n("Reassign")))
== KMessageBox::Continue;
}
//static
void KGlobalAccel::stealShortcutSystemwide(const QKeySequence &seq)
{
//get the shortcut, remove seq, and set the new shortcut
const QStringList actionId = self()->d->iface.action(seq[0]);
if (actionId.size() < 4) // not a global shortcut
return;
QList<int> sc = self()->d->iface.shortcut(actionId);
for (int i = 0; i < sc.count(); i++)
if (sc[i] == seq[0])
sc[i] = 0;
self()->d->iface.setForeignShortcut(actionId, sc);
}
#include "moc_kglobalaccel.cpp"