diff --git a/applets/appmenu/lib/appmenuapplet.cpp b/applets/appmenu/lib/appmenuapplet.cpp index 60b84ba26..14881eb5c 100644 --- a/applets/appmenu/lib/appmenuapplet.cpp +++ b/applets/appmenu/lib/appmenuapplet.cpp @@ -1,295 +1,293 @@ /* * Copyright 2016 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "appmenuapplet.h" #include "../plugin/appmenumodel.h" #include #include #include #include #include -#include -#include #include -#include -#include #include #include +#include +#include int AppMenuApplet::s_refs = 0; namespace { QString viewService() { return QStringLiteral("org.kde.kappmenuview"); } } AppMenuApplet::AppMenuApplet(QObject *parent, const QVariantList &data) : Plasma::Applet(parent, data) { ++s_refs; //if we're the first, regster the service if (s_refs == 1) { QDBusConnection::sessionBus().interface()->registerService(viewService(), QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } /*it registers or unregisters the service when the destroyed value of the applet change, and not in the dtor, because: when we "delete" an applet, it just hides it for about a minute setting its status to destroyed, in order to be able to do a clean undo: if we undo, there will be another destroyedchanged and destroyed will be false. When this happens, if we are the only appmenu applet existing, the dbus interface will have to be registered again*/ connect(this, &Applet::destroyedChanged, this, [this](bool destroyed) { if (destroyed) { //if we were the last, unregister if (--s_refs == 0) { QDBusConnection::sessionBus().interface()->unregisterService(viewService()); } } else { //if we're the first, regster the service if (++s_refs == 1) { QDBusConnection::sessionBus().interface()->registerService(viewService(), QDBusConnectionInterface::QueueService, QDBusConnectionInterface::DontAllowReplacement); } } }); } AppMenuApplet::~AppMenuApplet() = default; void AppMenuApplet::init() { } AppMenuModel *AppMenuApplet::model() const { return m_model; } void AppMenuApplet::setModel(AppMenuModel *model) { if (m_model != model) { m_model = model; emit modelChanged(); } } int AppMenuApplet::view() const { return m_viewType; } void AppMenuApplet::setView(int type) { if (m_viewType != type) { m_viewType = type; emit viewChanged(); } } int AppMenuApplet::currentIndex() const { return m_currentIndex; } void AppMenuApplet::setCurrentIndex(int currentIndex) { if (m_currentIndex != currentIndex) { m_currentIndex = currentIndex; emit currentIndexChanged(); } } QQuickItem *AppMenuApplet::buttonGrid() const { return m_buttonGrid; } void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid) { if (m_buttonGrid != buttonGrid) { m_buttonGrid = buttonGrid; emit buttonGridChanged(); } } QMenu *AppMenuApplet::createMenu(int idx) const { QMenu *menu = nullptr; QAction *action = nullptr; if (view() == CompactView) { menu = new QMenu(); for (int i=0; irowCount(); i++) { const QModelIndex index = m_model->index(i, 0); const QVariant data = m_model->data(index, AppMenuModel::ActionRole); action = (QAction *)data.value(); menu->addAction(action); } menu->setAttribute(Qt::WA_DeleteOnClose); } else if (view() == FullView) { const QModelIndex index = m_model->index(idx, 0); const QVariant data = m_model->data(index, AppMenuModel::ActionRole); action = (QAction *)data.value(); if (action) { menu = action->menu(); } } return menu; } void AppMenuApplet::onMenuAboutToHide() { setCurrentIndex(-1); } void AppMenuApplet::trigger(QQuickItem *ctx, int idx) { if (m_currentIndex == idx) { return; } if (!ctx || !ctx->window() || !ctx->window()->screen()) { return; } QMenu *actionMenu = createMenu(idx); if (actionMenu) { //this is a workaround where Qt will fail to realise a mouse has been released // this happens if a window which does not accept focus spawns a new window that takes focus and X grab // whilst the mouse is depressed // https://bugreports.qt.io/browse/QTBUG-59044 // this causes the next click to go missing //by releasing manually we avoid that situation auto ungrabMouseHack = [ctx]() { if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) { // FIXME event forge thing enters press and hold move mode :/ ctx->window()->mouseGrabberItem()->ungrabMouse(); } }; QTimer::singleShot(0, ctx, ungrabMouseHack); //end workaround const auto &geo = ctx->window()->screen()->availableVirtualGeometry(); QPoint pos = ctx->window()->mapToGlobal(ctx->mapToScene(QPointF()).toPoint()); if (location() == Plasma::Types::TopEdge) { pos.setY(pos.y() + ctx->height()); } actionMenu->adjustSize(); pos = QPoint(qBound(geo.x(), pos.x(), geo.x() + geo.width() - actionMenu->width()), qBound(geo.y(), pos.y(), geo.y() + geo.height() - actionMenu->height())); if (view() == FullView) { actionMenu->installEventFilter(this); } setStatus(Plasma::Types::AcceptingInputStatus); actionMenu->winId();//create window handle actionMenu->windowHandle()->setTransientParent(ctx->window()); actionMenu->popup(pos); //we can return to passive immediately, an autohide panel will stay open whilst //any transient window is showing setStatus(Plasma::Types::PassiveStatus); if (view() == FullView) { // hide the old menu only after showing the new one to avoid brief flickering // in other windows as they briefly re-gain focus QMenu *oldMenu = m_currentMenu; m_currentMenu = actionMenu; if (oldMenu && oldMenu != actionMenu) { oldMenu->hide(); } } setCurrentIndex(idx); // FIXME TODO connect only once connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection); return; } } // FIXME TODO doesn't work on submenu bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event) { auto *menu = qobject_cast(watched); if (!menu) { return false; } if (event->type() == QEvent::KeyPress) { auto *e = static_cast(event); // TODO right to left languages if (e->key() == Qt::Key_Left) { int desiredIndex = m_currentIndex - 1; emit requestActivateIndex(desiredIndex); return true; } else if (e->key() == Qt::Key_Right) { if (menu->activeAction() && menu->activeAction()->menu()) { return false; } int desiredIndex = m_currentIndex + 1; emit requestActivateIndex(desiredIndex); return true; } } else if (event->type() == QEvent::MouseMove) { auto *e = static_cast(event); if (!m_buttonGrid || !m_buttonGrid->window()) { return false; } // FIXME the panel margin breaks Fitt's law :( const QPointF &windowLocalPos = m_buttonGrid->window()->mapFromGlobal(e->globalPos()); const QPointF &buttonGridLocalPos = m_buttonGrid->mapFromScene(windowLocalPos); auto *item = m_buttonGrid->childAt(buttonGridLocalPos.x(), buttonGridLocalPos.y()); if (!item) { return false; } bool ok; const int buttonIndex = item->property("buttonIndex").toInt(&ok); if (!ok) { return false; } emit requestActivateIndex(buttonIndex); } return false; } K_EXPORT_PLASMA_APPLET_WITH_JSON(appmenu, AppMenuApplet, "metadata.json") #include "appmenuapplet.moc" diff --git a/applets/appmenu/plugin/appmenumodel.cpp b/applets/appmenu/plugin/appmenumodel.cpp index 0c5da77e2..388915cac 100644 --- a/applets/appmenu/plugin/appmenumodel.cpp +++ b/applets/appmenu/plugin/appmenumodel.cpp @@ -1,325 +1,324 @@ /****************************************************************** * Copyright 2016 Kai Uwe Broulik * Copyright 2016 Chinmoy Ranjan Pradhan * * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "appmenumodel.h" #include #if HAVE_X11 #include #include #endif #include -#include #include -#include #include #include #include +#include #include static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); #if HAVE_X11 static QHash s_atoms; #endif class KDBusMenuImporter : public DBusMenuImporter { public: KDBusMenuImporter(const QString &service, const QString &path, QObject *parent) : DBusMenuImporter(service, path, parent) { } protected: QIcon iconForName(const QString &name) override { return QIcon::fromTheme(name); } }; AppMenuModel::AppMenuModel(QObject *parent) : QAbstractListModel(parent), m_serviceWatcher(new QDBusServiceWatcher(this)) { connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &AppMenuModel::onActiveWindowChanged); connect(this, &AppMenuModel::modelNeedsUpdate, this, &AppMenuModel::update, Qt::UniqueConnection); onActiveWindowChanged(KWindowSystem::activeWindow()); m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); //if our current DBus connection gets lost, close the menu //we'll select the new menu when the focus changes connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) { if (serviceName == m_serviceName) { setMenuAvailable(false); emit modelNeedsUpdate(); } }); } AppMenuModel::~AppMenuModel() = default; bool AppMenuModel::menuAvailable() const { return m_menuAvailable; } void AppMenuModel::setMenuAvailable(bool set) { if (m_menuAvailable != set) { m_menuAvailable = set; emit menuAvailableChanged(); } } int AppMenuModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_activeMenu.count(); } void AppMenuModel::update() { beginResetModel(); if (!m_activeMenu.isEmpty() && !m_activeActions.isEmpty()) { m_activeMenu.clear(); m_activeActions.clear(); } if (m_menu && m_menuAvailable) { const auto &actions = m_menu->actions(); for (QAction *action : actions) { m_activeActions.append(action); m_activeMenu.append(action->text()); } } endResetModel(); } void AppMenuModel::onActiveWindowChanged(WId id) { qApp->removeNativeEventFilter(this); if (!id) { setMenuAvailable(false); emit modelNeedsUpdate(); return; } #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { auto *c = QX11Info::connection(); auto getWindowPropertyString = [c, this](WId id, const QByteArray &name) -> QByteArray { QByteArray value; if (!s_atoms.contains(name)) { const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(c, false, name.length(), name.constData()); QScopedPointer atomReply(xcb_intern_atom_reply(c, atomCookie, Q_NULLPTR)); if (atomReply.isNull()) { return value; } s_atoms[name] = atomReply->atom; if (s_atoms[name] == XCB_ATOM_NONE) { return value; } } static const long MAX_PROP_SIZE = 10000; auto propertyCookie = xcb_get_property(c, false, id, s_atoms[name], XCB_ATOM_STRING, 0, MAX_PROP_SIZE); QScopedPointer propertyReply(xcb_get_property_reply(c, propertyCookie, NULL)); if (propertyReply.isNull()) { return value; } if (propertyReply->type == XCB_ATOM_STRING && propertyReply->format == 8 && propertyReply->value_len > 0) { const char *data = (const char *) xcb_get_property_value(propertyReply.data()); int len = propertyReply->value_len; if (data) { value = QByteArray(data, data[len - 1] ? len : len - 1); } } return value; }; auto updateMenuFromWindowIfHasMenu = [this, &getWindowPropertyString](WId id) { const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_x11AppMenuServiceNamePropertyName)); const QString menuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_x11AppMenuObjectPathPropertyName)); if (!serviceName.isEmpty() && !menuObjectPath.isEmpty()) { updateApplicationMenu(serviceName, menuObjectPath); return true; } return false; }; KWindowInfo info(id, NET::WMState | NET::WMWindowType, NET::WM2TransientFor); if (info.hasState(NET::SkipTaskbar) || info.windowType(NET::UtilityMask) == NET::Utility || info.windowType(NET::DesktopMask) == NET::Desktop) { return; } WId transientId = info.transientFor(); // lok at transient windows first while (transientId) { if (updateMenuFromWindowIfHasMenu(transientId)) { return; } transientId = KWindowInfo(transientId, 0, NET::WM2TransientFor).transientFor(); } if (updateMenuFromWindowIfHasMenu(id)) { return; } // monitor whether an app menu becomes available later // this can happen when an app starts, shows its window, and only later announces global menu (e.g. Firefox) qApp->installNativeEventFilter(this); m_currentWindowId = id; //no menu found, set it to unavailable setMenuAvailable(false); emit modelNeedsUpdate(); } #endif } QHash AppMenuModel::roleNames() const { QHash roleNames; roleNames[MenuRole] = QByteArrayLiteral("activeMenu"); roleNames[ActionRole] = QByteArrayLiteral("activeActions"); return roleNames; } QVariant AppMenuModel::data(const QModelIndex &index, int role) const { int row = index.row(); if (row < 0 ) { return QVariant(); } if (role == MenuRole) { return m_activeMenu.at(row); } else if(role == ActionRole) { const QVariant data = qVariantFromValue((void *) m_activeActions.at(row)); return data; } return QVariant(); } void AppMenuModel::updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath) { if (m_serviceName == serviceName && m_menuObjectPath == menuObjectPath) { if (m_importer) { QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); } return; } m_serviceName = serviceName; m_serviceWatcher->setWatchedServices(QStringList({m_serviceName})); m_menuObjectPath = menuObjectPath; if (m_importer) { m_importer->deleteLater(); } m_importer = new KDBusMenuImporter(serviceName, menuObjectPath, this); QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); connect(m_importer.data(), &DBusMenuImporter::menuUpdated, this, [=](QMenu *menu) { m_menu = m_importer->menu(); if (m_menu.isNull() || menu != m_menu) { return; } //cache first layer of sub menus, which we'll be popping up for(QAction *a: m_menu->actions()) { if (a->menu()) { m_importer->updateMenu(a->menu()); } } setMenuAvailable(true); emit modelNeedsUpdate(); }); connect(m_importer.data(), &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) { // TODO submenus auto it = std::find(m_activeActions.constBegin(), m_activeActions.constEnd(), action); if (it != m_activeActions.constEnd()) { requestActivateIndex(it - m_activeActions.constBegin()); } }); } bool AppMenuModel::nativeEventFilter(const QByteArray &eventType, void *message, long *result) { Q_UNUSED(result); if (!KWindowSystem::isPlatformX11() || eventType != "xcb_generic_event_t") { return false; } #if HAVE_X11 auto e = static_cast(message); const uint8_t type = e->response_type & ~0x80; if (type == XCB_PROPERTY_NOTIFY) { auto *event = reinterpret_cast(e); if (event->window == m_currentWindowId) { auto serviceNameAtom = s_atoms.value(s_x11AppMenuServiceNamePropertyName); auto objectPathAtom = s_atoms.value(s_x11AppMenuObjectPathPropertyName); if (serviceNameAtom != XCB_ATOM_NONE && objectPathAtom != XCB_ATOM_NONE) { // shouldn't happen if (event->atom == serviceNameAtom || event->atom == objectPathAtom) { // see if we now have a menu onActiveWindowChanged(KWindowSystem::activeWindow()); } } } } #else Q_UNUSED(message); #endif return false; } diff --git a/applets/appmenu/plugin/appmenuplugin.cpp b/applets/appmenu/plugin/appmenuplugin.cpp index 893b3e0bc..d442c257f 100644 --- a/applets/appmenu/plugin/appmenuplugin.cpp +++ b/applets/appmenu/plugin/appmenuplugin.cpp @@ -1,32 +1,31 @@ /****************************************************************** * Copyright 2016 Chinmoy Ranjan Pradhan * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "appmenuplugin.h" #include "appmenumodel.h" - -#include #include + void AppmenuPlugin::registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.appmenu")); qmlRegisterType(uri, 1, 0, "AppMenuModel"); } diff --git a/applets/digital-clock/plugin/timezonemodel.cpp b/applets/digital-clock/plugin/timezonemodel.cpp index 1af838cb3..e7ee7ce0b 100644 --- a/applets/digital-clock/plugin/timezonemodel.cpp +++ b/applets/digital-clock/plugin/timezonemodel.cpp @@ -1,233 +1,232 @@ /*************************************************************************** * Copyright (C) 2014 Kai Uwe Broulik * * Copyright (C) 2014 Martin Klapetek * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "timezonemodel.h" #include "timezonesi18n.h" #include #include #include -#include TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent) : QSortFilterProxyModel(parent) { m_stringMatcher.setCaseSensitivity(Qt::CaseInsensitive); } bool TimeZoneFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if (!sourceModel() || m_filterString.isEmpty()) { return true; } const QString city = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CityRole).toString(); const QString region = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::RegionRole).toString(); const QString comment = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CommentRole).toString(); if (m_stringMatcher.indexIn(city) != -1 || m_stringMatcher.indexIn(region) != -1 || m_stringMatcher.indexIn(comment) != -1) { return true; } return false; } void TimeZoneFilterProxy::setFilterString(const QString &filterString) { m_filterString = filterString; m_stringMatcher.setPattern(filterString); emit filterStringChanged(); invalidateFilter(); } //============================================================================= TimeZoneModel::TimeZoneModel(QObject *parent) : QAbstractListModel(parent), m_timezonesI18n(new TimezonesI18n(this)) { update(); } TimeZoneModel::~TimeZoneModel() { } int TimeZoneModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_data.count(); } QVariant TimeZoneModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { TimeZoneData currentData = m_data.at(index.row()); switch(role) { case TimeZoneIdRole: return currentData.id; case RegionRole: return currentData.region; case CityRole: return currentData.city; case CommentRole: return currentData.comment; case CheckedRole: return currentData.checked; } } return QVariant(); } bool TimeZoneModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid() || value.isNull()) { return false; } if (role == CheckedRole) { m_data[index.row()].checked = value.toBool(); emit dataChanged(index, index); if (m_data[index.row()].checked) { m_selectedTimeZones.append(m_data[index.row()].id); m_offsetData.insert(m_data[index.row()].id, m_data[index.row()].offsetFromUtc); } else { m_selectedTimeZones.removeAll(m_data[index.row()].id); m_offsetData.remove(m_data[index.row()].id); } sortTimeZones(); emit selectedTimeZonesChanged(); return true; } return false; } void TimeZoneModel::update() { beginResetModel(); m_data.clear(); QTimeZone localZone = QTimeZone(QTimeZone::systemTimeZoneId()); const QStringList data = QString::fromUtf8(localZone.id()).split(QLatin1Char('/')); TimeZoneData local; local.id = QStringLiteral("Local"); local.region = i18nc("This means \"Local Timezone\"", "Local"); local.city = m_timezonesI18n->i18nCity(data.last()); local.comment = i18n("Your system time zone"); local.checked = false; m_data.append(local); QStringList cities; QHash zonesByCity; const QList systemTimeZones = QTimeZone::availableTimeZoneIds(); for (auto it = systemTimeZones.constBegin(); it != systemTimeZones.constEnd(); ++it) { const QTimeZone zone(*it); const QStringList splitted = QString::fromUtf8(zone.id()).split(QStringLiteral("/")); // CITY | COUNTRY | CONTINENT const QString key = QStringLiteral("%1|%2|%3").arg(splitted.last(), QLocale::countryToString(zone.country()), splitted.first()); cities.append(key); zonesByCity.insert(key, zone); } cities.sort(Qt::CaseInsensitive); Q_FOREACH (const QString &key, cities) { const QTimeZone timeZone = zonesByCity.value(key); QString comment = timeZone.comment(); if (!comment.isEmpty()) { comment = i18n(comment.toUtf8()); } const QStringList cityCountryContinent = key.split(QLatin1Char('|')); TimeZoneData newData; newData.id = timeZone.id(); newData.region = timeZone.country() == QLocale::AnyCountry ? QString() : m_timezonesI18n->i18nContinents(cityCountryContinent.at(2)) + QLatin1Char('/') + m_timezonesI18n->i18nCountry(timeZone.country()); newData.city = m_timezonesI18n->i18nCity(cityCountryContinent.at(0)); newData.comment = comment; newData.checked = false; newData.offsetFromUtc = timeZone.offsetFromUtc(QDateTime::currentDateTimeUtc()); m_data.append(newData); } endResetModel(); } void TimeZoneModel::setSelectedTimeZones(const QStringList &selectedTimeZones) { m_selectedTimeZones = selectedTimeZones; for (int i = 0; i < m_data.size(); i++) { if (m_selectedTimeZones.contains(m_data.at(i).id)) { m_data[i].checked = true; m_offsetData.insert(m_data[i].id, m_data[i].offsetFromUtc); QModelIndex index = createIndex(i, 0); emit dataChanged(index, index); } } sortTimeZones(); } void TimeZoneModel::selectLocalTimeZone() { m_data[0].checked = true; QModelIndex index = createIndex(0, 0); emit dataChanged(index, index); m_selectedTimeZones << m_data[0].id; emit selectedTimeZonesChanged(); } QHash TimeZoneModel::roleNames() const { return QHash({ {TimeZoneIdRole, "timeZoneId"}, {RegionRole, "region"}, {CityRole, "city"}, {CommentRole, "comment"}, {CheckedRole, "checked"} }); } void TimeZoneModel::sortTimeZones() { std::sort(m_selectedTimeZones.begin(), m_selectedTimeZones.end(), [this](const QString &a, const QString &b) { return m_offsetData.value(a) < m_offsetData.value(b); }); } diff --git a/applets/notifications/lib/notificationsapplet.cpp b/applets/notifications/lib/notificationsapplet.cpp index 5dc07792b..541b0e959 100644 --- a/applets/notifications/lib/notificationsapplet.cpp +++ b/applets/notifications/lib/notificationsapplet.cpp @@ -1,144 +1,142 @@ /* * Copyright 2014 (c) Martin Klapetek * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "notificationsapplet.h" #include -#include #include #include -#include NotificationsApplet::NotificationsApplet(QObject *parent, const QVariantList &data) : Plasma::Applet(parent, data), m_availableScreenRect(0,0,0,0) { } NotificationsApplet::~NotificationsApplet() { } void NotificationsApplet::init() { m_popupPosition = (NotificationsHelper::PositionOnScreen)configScreenPosition(); connect(this, &Plasma::Applet::locationChanged, this, &NotificationsApplet::onAppletLocationChanged); connect(containment(), &Plasma::Containment::screenChanged, this, &NotificationsApplet::onScreenChanges); Q_ASSERT(containment()); Q_ASSERT(containment()->corona()); connect(containment()->corona(), &Plasma::Corona::availableScreenRectChanged, this, &NotificationsApplet::onScreenChanges); Plasma::Applet::init(); onScreenChanges(); onAppletLocationChanged(); } void NotificationsApplet::onScreenChanges() { // when removing the panel the applet is in, the containment is being destroyed but its corona is still // there, rightfully emitting availableScreenRectChanged and then we blow up if we try to access it. if (!containment() || !containment()->corona()) { return; } auto newAvailableScreenRect = containment()->corona()->availableScreenRect(containment()->screen()); if (newAvailableScreenRect != m_availableScreenRect) { m_availableScreenRect = newAvailableScreenRect; Q_EMIT availableScreenRectChanged(m_availableScreenRect); } } QRect NotificationsApplet::availableScreenRect() const { return m_availableScreenRect; } void NotificationsApplet::onAppletLocationChanged() { if (configScreenPosition() == 0) { // If the screenPosition is set to default, // just follow the panel setScreenPositionFromAppletLocation(); } } uint NotificationsApplet::screenPosition() const { return m_popupPosition; } void NotificationsApplet::onScreenPositionChanged(uint position) { KConfigGroup globalGroup = globalConfig(); globalGroup.writeEntry("popupPosition", position); globalGroup.sync(); // If the position is set to default, let the setScreenPositionFromAppletLocation() // figure out the effective position, otherwise just set it to m_popupPosition // and emit the change if (position == NotificationsHelper::Default) { setScreenPositionFromAppletLocation(); } else if (m_popupPosition != position) { m_popupPosition = (NotificationsHelper::PositionOnScreen)position; Q_EMIT screenPositionChanged(m_popupPosition); } } uint NotificationsApplet::configScreenPosition() const { KConfigGroup globalGroup = globalConfig(); return globalGroup.readEntry("popupPosition", 0); //0 is default } void NotificationsApplet::setScreenPositionFromAppletLocation() { NotificationsHelper::PositionOnScreen newPopupPosition = m_popupPosition; if (location() == Plasma::Types::TopEdge) { if (QGuiApplication::isRightToLeft()) { newPopupPosition = NotificationsHelper::TopLeft; } else { newPopupPosition = NotificationsHelper::TopRight; } } else { if (QGuiApplication::isRightToLeft()) { newPopupPosition = NotificationsHelper::BottomLeft; } else { newPopupPosition = NotificationsHelper::BottomRight; } } if (newPopupPosition != m_popupPosition) { m_popupPosition = newPopupPosition; Q_EMIT screenPositionChanged(m_popupPosition); } } K_EXPORT_PLASMA_APPLET_WITH_JSON(notifications, NotificationsApplet, "metadata.json") #include "notificationsapplet.moc" diff --git a/applets/notifications/plugin/notificationshelper.cpp b/applets/notifications/plugin/notificationshelper.cpp index 3f861b32f..de5aa1fc2 100644 --- a/applets/notifications/plugin/notificationshelper.cpp +++ b/applets/notifications/plugin/notificationshelper.cpp @@ -1,329 +1,328 @@ /* Copyright (C) 2014 Martin Klapetek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "notificationshelper.h" #include #include #include #include -#include #include #include #include NotificationsHelper::NotificationsHelper(QObject *parent) : QObject(parent), m_popupLocation(NotificationsHelper::BottomRight), m_busy(false) { m_mutex = new QReadWriteLock(QReadWriteLock::Recursive); m_offset = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height(); m_dispatchTimer = new QTimer(this); m_dispatchTimer->setInterval(500); m_dispatchTimer->setSingleShot(true); connect(m_dispatchTimer, &QTimer::timeout, [this](){m_busy = false; processQueues();}); } NotificationsHelper::~NotificationsHelper() { qDeleteAll(m_availablePopups); qDeleteAll(m_popupsOnScreen); delete m_mutex; } void NotificationsHelper::setPopupLocation(PositionOnScreen popupLocation) { if (m_popupLocation != popupLocation) { m_popupLocation = popupLocation; emit popupLocationChanged(); repositionPopups(); } } void NotificationsHelper::setPlasmoidScreenGeometry(const QRect &plasmoidScreenGeometry) { m_plasmoidScreen = plasmoidScreenGeometry; repositionPopups(); } void NotificationsHelper::addNotificationPopup(QObject *win) { QQuickWindow *popup = qobject_cast(win); m_availablePopups.append(popup); // Don't let QML ever delete this component QQmlEngine::setObjectOwnership(win, QQmlEngine::CppOwnership); connect(win, SIGNAL(notificationTimeout()), this, SLOT(onPopupClosed())); connect(popup, &QWindow::heightChanged, this, &NotificationsHelper::repositionPopups, Qt::UniqueConnection); //We are sure that after visibleChanged the size is final //and the first expose event didn't arrive yet connect(popup, &QQuickWindow::visibleChanged, this, &NotificationsHelper::repositionPopups); } void NotificationsHelper::processQueues() { if (m_busy) { return; } m_mutex->lockForRead(); bool shouldProcessShow = !m_showQueue.isEmpty() && !m_availablePopups.isEmpty(); m_mutex->unlock(); if (shouldProcessShow) { m_busy = true; processShow(); // Return here, makes the movement more clear and easier to follow return; } m_mutex->lockForRead(); bool shouldProcessHide = !m_hideQueue.isEmpty(); m_mutex->unlock(); if (shouldProcessHide) { m_busy = true; processHide(); } } void NotificationsHelper::processShow() { m_mutex->lockForWrite(); const QVariantMap notificationData = m_showQueue.takeFirst(); m_mutex->unlock(); QString sourceName = notificationData.value(QStringLiteral("source")).toString(); // Try getting existing popup for the given source // (case of notification being just updated) QQuickWindow *popup = m_sourceMap.value(sourceName); if (!popup) { // No existing notification for the given source, // take one from the available popups m_mutex->lockForWrite(); popup = m_availablePopups.takeFirst(); m_popupsOnScreen << popup; m_sourceMap.insert(sourceName, popup); m_mutex->unlock(); // Set the source name directly on the popup object too // to avoid looking up the notificationProperties map as above popup->setProperty("sourceName", sourceName); } // Populate the popup with data, this is the component's own QML method QMetaObject::invokeMethod(popup, "populatePopup", Qt::DirectConnection, Q_ARG(QVariant, notificationData)); Q_EMIT popupShown(popup); //use setproperty so the Dialog reimplementation will be used popup->setProperty("visible", true); if (!m_dispatchTimer->isActive()) { m_dispatchTimer->start(); } } void NotificationsHelper::processHide() { m_mutex->lockForWrite(); QQuickWindow *popup = m_hideQueue.takeFirst(); m_mutex->unlock(); if (popup) { m_mutex->lockForWrite(); // Remove the popup from the active list and return it into the available list m_popupsOnScreen.removeAll(popup); m_sourceMap.remove(popup->property("sourceName").toString()); if (!m_availablePopups.contains(popup)) { // make extra sure that pointers in here aren't doubled m_availablePopups.append(popup); } m_mutex->unlock(); popup->hide(); QMetaObject::invokeMethod(popup, "clearPopup", Qt::DirectConnection); } m_mutex->lockForRead(); bool shouldReposition = !m_popupsOnScreen.isEmpty();// && m_showQueue.isEmpty(); m_mutex->unlock(); if (shouldReposition) { repositionPopups(); } if (!m_dispatchTimer->isActive()) { m_dispatchTimer->start(); } } void NotificationsHelper::displayNotification(const QVariantMap ¬ificationData) { if (notificationData.isEmpty()) { return; } QVariant sourceName = notificationData.value(QStringLiteral("source")); // first check if we don't already have data for the same source // which would mean that the notification was just updated // so remove the old one and append the newest data only QMutableListIterator i(m_showQueue); while (i.hasNext()) { if (i.next().value(QStringLiteral("source")) == sourceName) { m_mutex->lockForWrite(); i.remove(); m_mutex->unlock(); } } // ...also look into the hide queue, if it's already queued // for hiding, we need to remove it from there otherwise // it will get closed too soon QMutableListIterator j(m_hideQueue); while (j.hasNext()) { if (j.next()->property("sourceName") == sourceName) { m_mutex->lockForWrite(); j.remove(); m_mutex->unlock(); } } m_mutex->lockForWrite(); m_showQueue.append(notificationData); m_mutex->unlock(); if (!m_dispatchTimer->isActive()) { // If the dispatch timer is not already running, process // the queues directly, that should cut the time between // notification emitting the event and popup displaying processQueues(); } } void NotificationsHelper::closePopup(const QString &sourceName) { QQuickWindow *popup = m_sourceMap.value(sourceName); m_mutex->lockForRead(); bool shouldQueue = popup && !m_hideQueue.contains(popup); m_mutex->unlock(); // Make sure the notification that was closed (programatically) // is not in the show queue. This is important otherwise that // notification will be shown and then never closed (because // the close event arrives here, before it's even shown) QMutableListIterator i(m_showQueue); while (i.hasNext()) { if (i.next().value(QStringLiteral("source")) == sourceName) { m_mutex->lockForWrite(); i.remove(); m_mutex->unlock(); } } if (shouldQueue) { m_mutex->lockForWrite(); m_hideQueue.append(popup); m_mutex->unlock(); if (!m_dispatchTimer->isActive()) { processQueues(); } } } void NotificationsHelper::onPopupClosed() { QQuickWindow *popup = qobject_cast(sender()); m_mutex->lockForRead(); bool shouldQueue = popup && !m_hideQueue.contains(popup); m_mutex->unlock(); if (shouldQueue) { m_mutex->lockForWrite(); m_hideQueue << popup; m_mutex->unlock(); if (!m_dispatchTimer->isActive()) { processQueues(); } } } void NotificationsHelper::repositionPopups() { int cumulativeHeight = m_offset; m_mutex->lockForWrite(); QPoint pos; for (int i = 0; i < m_popupsOnScreen.size(); ++i) { if (m_popupLocation == NotificationsHelper::TopLeft || m_popupLocation == NotificationsHelper::TopCenter || m_popupLocation == NotificationsHelper::TopRight) { pos.setY(m_plasmoidScreen.top() + cumulativeHeight); } else { pos.setY(m_plasmoidScreen.bottom() - cumulativeHeight - m_popupsOnScreen[i]->height()); } switch (m_popupLocation) { case Default: //This should not happen as the defualt handling is in NotificationApplet::onScreenPositionChanged Q_ASSERT(false); qWarning("Notication popupLocation is still \"default\". This should not happen"); //fall through to top right case TopRight: case BottomRight: pos.setX(m_plasmoidScreen.right() - m_popupsOnScreen[i]->width() - m_offset); break; case TopCenter: case BottomCenter: pos.setX(m_plasmoidScreen.x() + (m_plasmoidScreen.width() / 2) - (m_popupsOnScreen[i]->width() / 2)); break; case TopLeft: case BottomLeft: pos.setX(m_plasmoidScreen.left() + m_offset); break; case Left: case Center: case Right: // Fall-through to make the compiler happy break; } m_popupsOnScreen[i]->setPosition(pos); cumulativeHeight += (m_popupsOnScreen[i]->height() + m_offset); } m_mutex->unlock(); } diff --git a/applets/notifications/plugin/notificationshelperplugin.cpp b/applets/notifications/plugin/notificationshelperplugin.cpp index 48ae488ac..ae29b461c 100644 --- a/applets/notifications/plugin/notificationshelperplugin.cpp +++ b/applets/notifications/plugin/notificationshelperplugin.cpp @@ -1,84 +1,84 @@ /* Copyright (C) 2014 Martin Klapetek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "notificationshelperplugin.h" #include "notificationshelper.h" #include "thumbnailer.h" #include "draghelper.h" -#include -#include #include #include +#include +#include class NoAccessNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { public: QNetworkAccessManager *create(QObject *parent) override { QNetworkAccessManager *manager = new QNetworkAccessManager(parent); manager->setNetworkAccessible(QNetworkAccessManager::NotAccessible); return manager; } }; class UrlHelper : public QObject { Q_OBJECT public: Q_INVOKABLE bool isUrlValid(const QString &url) const { return QUrl::fromUserInput(url).isValid(); } }; static QObject *urlcheck_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return new UrlHelper(); } static QObject *draghelper_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return new DragHelper(); } void NotificationsHelperPlugin::registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.notifications")); qmlRegisterType(uri, 1, 0, "NotificationsHelper"); qmlRegisterType(uri, 1, 0, "Thumbnailer"); qmlRegisterSingletonType(uri, 1, 0, "UrlHelper", urlcheck_singletontype_provider); qmlRegisterSingletonType(uri, 1, 0, "DragHelper", draghelper_singletontype_provider); } void NotificationsHelperPlugin::initializeEngine(QQmlEngine *engine, const char *uri) { Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.notifications")); auto oldFactory = engine->networkAccessManagerFactory(); engine->setNetworkAccessManagerFactory(nullptr); delete oldFactory; engine->setNetworkAccessManagerFactory(new NoAccessNetworkAccessManagerFactory); } #include "notificationshelperplugin.moc" diff --git a/applets/notifications/plugin/thumbnailer.cpp b/applets/notifications/plugin/thumbnailer.cpp index 6ec80cdf9..72a64323a 100644 --- a/applets/notifications/plugin/thumbnailer.cpp +++ b/applets/notifications/plugin/thumbnailer.cpp @@ -1,242 +1,241 @@ /* Copyright (C) 2016 Kai Uwe Broulik This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "thumbnailer.h" #include #include #include -#include #include #include #include #include -#include #include +#include #include #include #include #include #include #include #include Thumbnailer::Thumbnailer(QObject *parent) : QObject(parent) { } Thumbnailer::~Thumbnailer() = default; void Thumbnailer::classBegin() { } void Thumbnailer::componentComplete() { m_inited = true; generatePreview(); } QUrl Thumbnailer::url() const { return m_url; } void Thumbnailer::setUrl(const QUrl &url) { if (m_url != url) { m_url = url; emit urlChanged(); generatePreview(); } } QSize Thumbnailer::size() const { return m_size; } void Thumbnailer::setSize(const QSize &size) { if (m_size != size) { m_size = size; emit sizeChanged(); generatePreview(); } } bool Thumbnailer::hasPreview() const { return !m_pixmap.isNull(); } QPixmap Thumbnailer::pixmap() const { return m_pixmap; } QSize Thumbnailer::pixmapSize() const { return m_pixmap.size(); } QString Thumbnailer::iconName() const { return m_iconName; } bool Thumbnailer::menuVisible() const { return m_menuVisible; } void Thumbnailer::showContextMenu(int x, int y, const QString &path, QQuickItem *ctx) { if (!ctx || !ctx->window()) { return; } const QUrl url(path); if (!url.isValid()) { return; } KFileItem fileItem(url); QMenu *menu = new QMenu(); menu->setAttribute(Qt::WA_DeleteOnClose, true); connect(menu, &QMenu::aboutToHide, this, [this] { m_menuVisible = false; emit menuVisibleChanged(); }); if (KProtocolManager::supportsListing(url)) { QAction *openContainingFolderAction = menu->addAction(QIcon::fromTheme("folder-open"), i18n("Open Containing Folder")); connect(openContainingFolderAction, &QAction::triggered, [url] { KIO::highlightInFileManager({url}); }); } menu->addSeparator(); // KStandardAction? But then the Ctrl+C shortcut makes no sense in this context QAction *copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy")); connect(copyAction, &QAction::triggered, [fileItem] { // inspired by KDirModel::mimeData() QMimeData *data = new QMimeData(); // who cleans it up? KUrlMimeData::setUrls({fileItem.url()}, {fileItem.mostLocalUrl()}, data); QApplication::clipboard()->setMimeData(data); }); KFileItemActions *actions = new KFileItemActions(menu); KFileItemListProperties itemProperties(KFileItemList({fileItem})); actions->setItemListProperties(itemProperties); actions->addOpenWithActionsTo(menu); actions->addServiceActionsTo(menu); actions->addPluginActionsTo(menu); QAction *propertiesAction = menu->addAction(QIcon::fromTheme("document-properties"), i18n("Properties")); connect(propertiesAction, &QAction::triggered, [fileItem] { KPropertiesDialog *dialog = new KPropertiesDialog(fileItem.url()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); }); //this is a workaround where Qt will fail to realise a mouse has been released // this happens if a window which does not accept focus spawns a new window that takes focus and X grab // whilst the mouse is depressed // https://bugreports.qt.io/browse/QTBUG-59044 // this causes the next click to go missing //by releasing manually we avoid that situation auto ungrabMouseHack = [ctx]() { if (ctx->window()->mouseGrabberItem()) { ctx->window()->mouseGrabberItem()->ungrabMouse(); } }; QTimer::singleShot(0, ctx, ungrabMouseHack); //end workaround QPoint pos; if (x == -1 && y == -1) { // align "bottom left of ctx" menu->adjustSize(); pos = ctx->mapToGlobal(QPointF(0, ctx->height())).toPoint(); if (!qApp->isRightToLeft()) { pos.rx() += ctx->width(); pos.rx() -= menu->width(); } } else { pos = ctx->mapToGlobal(QPointF(x, y)).toPoint(); } menu->popup(pos); m_menuVisible = true; emit menuVisibleChanged(); } void Thumbnailer::generatePreview() { if (!m_inited) { return; } if (!m_url.isValid() || !m_url.isLocalFile() || !m_size.isValid()) { return; } auto maxSize = qMax(m_size.width(), m_size.height()); KIO::PreviewJob *job = KIO::filePreview(KFileItemList({KFileItem(m_url)}), QSize(maxSize,maxSize)); job->setIgnoreMaximumSize(true); connect(job, &KIO::PreviewJob::gotPreview, this, [this](const KFileItem &item, const QPixmap &preview) { Q_UNUSED(item); m_pixmap = preview; emit pixmapChanged(); if (!m_iconName.isEmpty()) { m_iconName.clear(); emit iconNameChanged(); } }); connect(job, &KIO::PreviewJob::failed, this, [this](const KFileItem &item) { m_pixmap = QPixmap(); emit pixmapChanged(); const QString &iconName = item.determineMimeType().iconName(); if (m_iconName != iconName) { m_iconName = iconName; emit iconNameChanged(); } }); job->start(); } diff --git a/applets/systemtray/container/systemtraycontainer.cpp b/applets/systemtray/container/systemtraycontainer.cpp index f64ace78d..4222957ec 100644 --- a/applets/systemtray/container/systemtraycontainer.cpp +++ b/applets/systemtray/container/systemtraycontainer.cpp @@ -1,148 +1,147 @@ /*************************************************************************** * Copyright (C) 2015 Marco Martin * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "systemtraycontainer.h" #include "debug.h" -#include #include #include #include #include SystemTrayContainer::SystemTrayContainer(QObject *parent, const QVariantList &args) : Plasma::Applet(parent, args) { } SystemTrayContainer::~SystemTrayContainer() { if (destroyed()) { m_innerContainment->destroy(); } } void SystemTrayContainer::init() { Applet::init(); //in the first creation we immediately create the systray: so it's accessible during desktop scripting uint id = config().readEntry("SystrayContainmentId", 0); if (id == 0) { ensureSystrayExists(); } } void SystemTrayContainer::ensureSystrayExists() { if (m_innerContainment) { return; } Plasma::Containment *cont = containment(); if (!cont) { return; } Plasma::Corona *c = cont->corona(); if (!c) { return; } uint id = config().readEntry("SystrayContainmentId", 0); if (id > 0) { foreach (Plasma::Containment *candidate, c->containments()) { if (candidate->id() == id) { m_innerContainment = candidate; break; } } qCDebug(SYSTEM_TRAY_CONTAINER) << "Containment id" << id << "that used to be a system tray was deleted"; //id = 0; } if (!m_innerContainment) { m_innerContainment = c->createContainment("org.kde.plasma.private.systemtray", QVariantList() << "org.kde.plasma:force-create"); config().writeEntry("SystrayContainmentId", m_innerContainment->id()); } if (!m_innerContainment) { return; } m_innerContainment->setParent(this); connect(containment(), &Plasma::Containment::screenChanged, m_innerContainment.data(), &Plasma::Containment::reactToScreenChange); if (formFactor() == Plasma::Types::Horizontal || formFactor() == Plasma::Types::Vertical) { m_innerContainment->setFormFactor(formFactor()); } else { m_innerContainment->setFormFactor(Plasma::Types::Horizontal); } m_innerContainment->setLocation(location()); m_internalSystray = m_innerContainment->property("_plasma_graphicObject").value(); emit internalSystrayChanged(); actions()->addAction("configure", m_innerContainment->actions()->action("configure")); connect(m_innerContainment.data(), &Plasma::Containment::configureRequested, this, [this](Plasma::Applet *applet) { emit containment()->configureRequested(applet); } ); if (m_internalSystray) { //don't let internal systray manage context menus m_internalSystray->setAcceptedMouseButtons(Qt::NoButton); } //replace internal remove action with ours m_innerContainment->actions()->addAction("remove", actions()->action("remove")); } void SystemTrayContainer::constraintsEvent(Plasma::Types::Constraints constraints) { if (constraints & Plasma::Types::LocationConstraint) { if (m_innerContainment) { m_innerContainment->setLocation(location()); } } if (constraints & Plasma::Types::FormFactorConstraint) { if (m_innerContainment) { if (formFactor() == Plasma::Types::Horizontal || formFactor() == Plasma::Types::Vertical) { m_innerContainment->setFormFactor(formFactor()); } else { m_innerContainment->setFormFactor(Plasma::Types::Horizontal); } } } if (constraints & Plasma::Types::UiReadyConstraint) { ensureSystrayExists(); } } QQuickItem *SystemTrayContainer::internalSystray() { return m_internalSystray; } K_EXPORT_PLASMA_APPLET_WITH_JSON(systemtraycontainer, SystemTrayContainer, "metadata.json") #include "systemtraycontainer.moc" diff --git a/applets/systemtray/systemtray.cpp b/applets/systemtray/systemtray.cpp index 24634ad59..9e94866de 100644 --- a/applets/systemtray/systemtray.cpp +++ b/applets/systemtray/systemtray.cpp @@ -1,581 +1,580 @@ /*************************************************************************** * Copyright (C) 2015 Marco Martin * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "systemtray.h" #include "debug.h" #include -#include #include #include #include #include #include #include -#include #include #include #include +#include #include #include -#include #include +#include #include #include class PlasmoidModel: public QStandardItemModel { public: explicit PlasmoidModel(QObject *parent = nullptr) : QStandardItemModel(parent) { } QHash roleNames() const override { QHash roles = QStandardItemModel::roleNames(); roles[Qt::UserRole+1] = "plugin"; return roles; } }; SystemTray::SystemTray(QObject *parent, const QVariantList &args) : Plasma::Containment(parent, args), m_availablePlasmoidsModel(nullptr) { setHasConfigurationInterface(true); setContainmentType(Plasma::Types::CustomEmbeddedContainment); } SystemTray::~SystemTray() { } void SystemTray::init() { Containment::init(); for (const auto &info: Plasma::PluginLoader::self()->listAppletMetaData(QString())) { if (!info.isValid() || info.value(QStringLiteral("X-Plasma-NotificationArea")) != "true") { continue; } m_systrayApplets[info.pluginId()] = KPluginInfo(info); if (info.isEnabledByDefault()) { m_defaultPlasmoids += info.pluginId(); } const QString dbusactivation = info.value(QStringLiteral("X-Plasma-DBusActivationService")); if (!dbusactivation.isEmpty()) { qCDebug(SYSTEM_TRAY) << "ST Found DBus-able Applet: " << info.pluginId() << dbusactivation; m_dbusActivatableTasks[info.pluginId()] = dbusactivation; } } } void SystemTray::newTask(const QString &task) { foreach (Plasma::Applet *applet, applets()) { if (!applet->pluginMetaData().isValid()) { continue; } //only allow one instance per applet if (task == applet->pluginMetaData().pluginId()) { //Applet::destroy doesn't delete the applet from Containment::applets in the same event //potentially a dbus activated service being restarted can be added in this time. if (!applet->destroyed()) { return; } } } //known one, recycle the id to reuse old config if (m_knownPlugins.contains(task)) { Applet *applet = Plasma::PluginLoader::self()->loadApplet(task, m_knownPlugins.value(task), QVariantList()); //this should never happen unless explicitly wrong config is hand-written or //(more likely) a previously added applet is uninstalled if (!applet) { qWarning() << "Unable to find applet" << task; return; } applet->setProperty("org.kde.plasma:force-create", true); addApplet(applet); //create a new one automatic id, new config group } else { Applet * applet = createApplet(task, QVariantList() << "org.kde.plasma:force-create"); if (applet) { m_knownPlugins[task] = applet->id(); } } } void SystemTray::cleanupTask(const QString &task) { foreach (Plasma::Applet *applet, applets()) { if (applet->pluginMetaData().isValid() && task == applet->pluginMetaData().pluginId()) { //we are *not* cleaning the config here, because since is one //of those automatically loaded/unloaded by dbus, we want to recycle //the config the next time it's loaded, in case the user configured something here applet->deleteLater(); //HACK: we need to remove the applet from Containment::applets() as soon as possible //otherwise we may have disappearing applets for restarting dbus services //this may be removed when we depend from a frameworks version in which appletDeleted is emitted as soon as deleteLater() is called emit appletDeleted(applet); } } } void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface, int x, int y) { if (!appletInterface) { return; } Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); QPointF pos = appletInterface->mapToScene(QPointF(x, y)); if (appletInterface->window() && appletInterface->window()->screen()) { pos = appletInterface->window()->mapToGlobal(pos.toPoint()); } else { pos = QPoint(); } QMenu *desktopMenu = new QMenu; connect(this, &QObject::destroyed, desktopMenu, &QMenu::close); desktopMenu->setAttribute(Qt::WA_DeleteOnClose); //this is a workaround where Qt will fail to realise a mouse has been released // this happens if a window which does not accept focus spawns a new window that takes focus and X grab // whilst the mouse is depressed // https://bugreports.qt.io/browse/QTBUG-59044 // this causes the next click to go missing //by releasing manually we avoid that situation auto ungrabMouseHack = [appletInterface]() { if (appletInterface->window() && appletInterface->window()->mouseGrabberItem()) { appletInterface->window()->mouseGrabberItem()->ungrabMouse(); } }; QTimer::singleShot(0, appletInterface, ungrabMouseHack); //end workaround emit applet->contextualActionsAboutToShow(); foreach (QAction *action, applet->contextualActions()) { if (action) { desktopMenu->addAction(action); } } QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application")); if (runAssociatedApplication && runAssociatedApplication->isEnabled()) { desktopMenu->addAction(runAssociatedApplication); } if (applet->actions()->action(QStringLiteral("configure"))) { desktopMenu->addAction(applet->actions()->action(QStringLiteral("configure"))); } if (desktopMenu->isEmpty()) { delete desktopMenu; return; } desktopMenu->adjustSize(); if (QScreen *screen = appletInterface->window()->screen()) { const QRect geo = screen->availableGeometry(); pos = QPoint(qBound(geo.left(), (int)pos.x(), geo.right() - desktopMenu->width()), qBound(geo.top(), (int)pos.y(), geo.bottom() - desktopMenu->height())); } KAcceleratorManager::manage(desktopMenu); desktopMenu->winId(); desktopMenu->windowHandle()->setTransientParent(appletInterface->window()); desktopMenu->popup(pos.toPoint()); } QString SystemTray::plasmoidCategory(QQuickItem *appletInterface) const { if (!appletInterface) { return QStringLiteral("UnknownCategory"); } Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); if (!applet || !applet->pluginMetaData().isValid()) { return QStringLiteral("UnknownCategory"); } const QString cat = applet->pluginMetaData().value(QStringLiteral("X-Plasma-NotificationAreaCategory")); if (cat.isEmpty()) { return QStringLiteral("UnknownCategory"); } return cat; } void SystemTray::showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon) { if (QCoreApplication::closingDown() || !statusNotifierIcon) { // apparently an edge case can be triggered due to the async nature of all this // see: https://bugs.kde.org/show_bug.cgi?id=251977 return; } Plasma::ServiceJob *sjob = qobject_cast(job); if (!sjob) { return; } QMenu *menu = qobject_cast(sjob->result().value()); if (menu) { menu->adjustSize(); const auto parameters = sjob->parameters(); int x = parameters[QStringLiteral("x")].toInt(); int y = parameters[QStringLiteral("y")].toInt(); //try tofind the icon screen coordinates, and adjust the position as a poor //man's popupPosition QRect screenItemRect(statusNotifierIcon->mapToScene(QPointF(0, 0)).toPoint(), QSize(statusNotifierIcon->width(), statusNotifierIcon->height())); if (statusNotifierIcon->window()) { screenItemRect.moveTopLeft(statusNotifierIcon->window()->mapToGlobal(screenItemRect.topLeft())); } switch (location()) { case Plasma::Types::LeftEdge: x = screenItemRect.right(); y = screenItemRect.top(); break; case Plasma::Types::RightEdge: x = screenItemRect.left() - menu->width(); y = screenItemRect.top(); break; case Plasma::Types::TopEdge: x = screenItemRect.left(); y = screenItemRect.bottom(); break; case Plasma::Types::BottomEdge: x = screenItemRect.left(); y = screenItemRect.top() - menu->height(); break; default: x = screenItemRect.left(); if (screenItemRect.top() - menu->height() >= statusNotifierIcon->window()->screen()->geometry().top()) { y = screenItemRect.top() - menu->height(); } else { y = screenItemRect.bottom(); } } KAcceleratorManager::manage(menu); menu->popup(QPoint(x, y)); } } QPointF SystemTray::popupPosition(QQuickItem* visualParent, int x, int y) { if (!visualParent) { return QPointF(0, 0); } QPointF pos = visualParent->mapToScene(QPointF(x, y)); if (visualParent->window() && visualParent->window()->screen()) { pos = visualParent->window()->mapToGlobal(pos.toPoint()); } else { return QPoint(); } return pos; } void SystemTray::reorderItemBefore(QQuickItem* before, QQuickItem* after) { if (!before || !after) { return; } before->setVisible(false); before->setParentItem(after->parentItem()); before->stackBefore(after); before->setVisible(true); } void SystemTray::reorderItemAfter(QQuickItem* after, QQuickItem* before) { if (!before || !after) { return; } after->setVisible(false); after->setParentItem(before->parentItem()); after->stackAfter(before); after->setVisible(true); } bool SystemTray::isSystemTrayApplet(const QString &appletId) { return m_systrayApplets.contains(appletId); } void SystemTray::restoreContents(KConfigGroup &group) { Q_UNUSED(group); //NOTE: RestoreContents shouldnn't do anything here because is too soon, so have an empty reimplementation } void SystemTray::restorePlasmoids() { if (!isContainment()) { qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened"; return; } //First: remove all that are not allowed anymore foreach (Plasma::Applet *applet, applets()) { //Here it should always be valid. //for some reason it not always is. if (!applet->pluginMetaData().isValid()) { applet->config().parent().deleteGroup(); applet->deleteLater(); } else { const QString task = applet->pluginMetaData().pluginId(); if (!m_allowedPlasmoids.contains(task)) { //in those cases we do delete the applet config completely //as they were explicitly disabled by the user applet->config().parent().deleteGroup(); applet->deleteLater(); } } } KConfigGroup cg = config(); cg = KConfigGroup(&cg, "Applets"); foreach (const QString &group, cg.groupList()) { KConfigGroup appletConfig(&cg, group); QString plugin = appletConfig.readEntry("plugin"); if (!plugin.isEmpty()) { m_knownPlugins[plugin] = group.toInt(); } } QStringList ownApplets; QMap sortedApplets; for (auto it = m_systrayApplets.constBegin(); it != m_systrayApplets.constEnd(); ++it) { const KPluginInfo &info = it.value(); if (m_allowedPlasmoids.contains(info.pluginName()) && ! m_dbusActivatableTasks.contains(info.pluginName())) { //FIXME // if we already have a plugin with this exact name in it, then check if it is the // same plugin and skip it if it is indeed already listed if (sortedApplets.contains(info.name())) { bool dupe = false; // it is possible (though poor form) to have multiple applets // with the same visible name but different plugins, so we hve to check all values foreach (const KPluginInfo &existingInfo, sortedApplets.values(info.name())) { if (existingInfo.pluginName() == info.pluginName()) { dupe = true; break; } } if (dupe) { continue; } } // insertMulti becase it is possible (though poor form) to have multiple applets // with the same visible name but different plugins sortedApplets.insertMulti(info.name(), info); } } foreach (const KPluginInfo &info, sortedApplets) { qCDebug(SYSTEM_TRAY) << " Adding applet: " << info.name(); if (m_allowedPlasmoids.contains(info.pluginName())) { newTask(info.pluginName()); } } initDBusActivatables(); } QStringList SystemTray::defaultPlasmoids() const { return m_defaultPlasmoids; } QAbstractItemModel* SystemTray::availablePlasmoids() { if (!m_availablePlasmoidsModel) { m_availablePlasmoidsModel = new PlasmoidModel(this); foreach (const KPluginInfo &info, m_systrayApplets) { QString name = info.name(); const QString dbusactivation = info.property(QStringLiteral("X-Plasma-DBusActivationService")).toString(); if (!dbusactivation.isEmpty()) { name += i18n(" (Automatic load)"); } QStandardItem *item = new QStandardItem(QIcon::fromTheme(info.icon()), name); item->setData(info.pluginName()); m_availablePlasmoidsModel->appendRow(item); } m_availablePlasmoidsModel->sort(0 /*column*/); } return m_availablePlasmoidsModel; } QStringList SystemTray::allowedPlasmoids() const { return m_allowedPlasmoids; } void SystemTray::setAllowedPlasmoids(const QStringList &allowed) { if (allowed == m_allowedPlasmoids) { return; } m_allowedPlasmoids = allowed; restorePlasmoids(); emit allowedPlasmoidsChanged(); } void SystemTray::initDBusActivatables() { /* Loading and unloading Plasmoids when dbus services come and go * * This works as follows: * - we collect a list of plugins and related services in m_dbusActivatableTasks * - we query DBus for the list of services, async (initDBusActivatables()) * - we go over that list, adding tasks when a service and plugin match (serviceNameFetchFinished()) * - we start watching for new services, and do the same (serviceNameFetchFinished()) * - whenever a service is gone, we check whether to unload a Plasmoid (serviceUnregistered()) */ QDBusPendingCall async = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames")); QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); connect(callWatcher, &QDBusPendingCallWatcher::finished, [=](QDBusPendingCallWatcher *callWatcher){ SystemTray::serviceNameFetchFinished(callWatcher, QDBusConnection::sessionBus()); }); QDBusPendingCall systemAsync = QDBusConnection::systemBus().interface()->asyncCall(QStringLiteral("ListNames")); QDBusPendingCallWatcher *systemCallWatcher = new QDBusPendingCallWatcher(systemAsync, this); connect(systemCallWatcher, &QDBusPendingCallWatcher::finished, [=](QDBusPendingCallWatcher *callWatcher){ SystemTray::serviceNameFetchFinished(callWatcher, QDBusConnection::systemBus()); }); } void SystemTray::serviceNameFetchFinished(QDBusPendingCallWatcher* watcher, const QDBusConnection &connection) { QDBusPendingReply propsReply = *watcher; watcher->deleteLater(); if (propsReply.isError()) { qCWarning(SYSTEM_TRAY) << "Could not get list of available D-Bus services"; } else { foreach (const QString& serviceName, propsReply.value()) { serviceRegistered(serviceName); } } // Watch for new services // We need to watch for all of new services here, since we want to "match" the names, // not just compare them // This makes mpris work, since it wants to match org.mpris.MediaPlayer2.dragonplayer // against org.mpris.MediaPlayer2 // QDBusServiceWatcher is not capable for watching wildcard service right now // See: // https://bugreports.qt.io/browse/QTBUG-51683 // https://bugreports.qt.io/browse/QTBUG-33829 connect(connection.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &SystemTray::serviceOwnerChanged); } void SystemTray::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) { if (oldOwner.isEmpty()) { serviceRegistered(serviceName); } else if (newOwner.isEmpty()) { serviceUnregistered(serviceName); } } void SystemTray::serviceRegistered(const QString &service) { //qCDebug(SYSTEM_TRAY) << "DBus service appeared:" << service; for (auto it = m_dbusActivatableTasks.constBegin(), end = m_dbusActivatableTasks.constEnd(); it != end; ++it) { const QString &plugin = it.key(); if (!m_allowedPlasmoids.contains(plugin)) { continue; } const QString &pattern = it.value(); QRegExp rx(pattern); rx.setPatternSyntax(QRegExp::Wildcard); if (rx.exactMatch(service)) { //qCDebug(SYSTEM_TRAY) << "ST : DBus service " << m_dbusActivatableTasks[plugin] << "appeared. Loading " << plugin; newTask(plugin); m_dbusServiceCounts[plugin]++; } } } void SystemTray::serviceUnregistered(const QString &service) { //qCDebug(SYSTEM_TRAY) << "DBus service disappeared:" << service; for (auto it = m_dbusActivatableTasks.constBegin(), end = m_dbusActivatableTasks.constEnd(); it != end; ++it) { const QString &plugin = it.key(); if (!m_allowedPlasmoids.contains(plugin)) { continue; } const QString &pattern = it.value(); QRegExp rx(pattern); rx.setPatternSyntax(QRegExp::Wildcard); if (rx.exactMatch(service)) { m_dbusServiceCounts[plugin]--; Q_ASSERT(m_dbusServiceCounts[plugin] >= 0); if (m_dbusServiceCounts[plugin] == 0) { //qCDebug(SYSTEM_TRAY) << "ST : DBus service " << m_dbusActivatableTasks[plugin] << " disappeared. Unloading " << plugin; cleanupTask(plugin); } } } } K_EXPORT_PLASMA_APPLET_WITH_JSON(systemtray, SystemTray, "metadata.json") #include "systemtray.moc" diff --git a/applets/systemtray/tests/statusnotifier/main.cpp b/applets/systemtray/tests/statusnotifier/main.cpp index dd045c3ff..f6b1c101b 100644 --- a/applets/systemtray/tests/statusnotifier/main.cpp +++ b/applets/systemtray/tests/statusnotifier/main.cpp @@ -1,46 +1,45 @@ /* * Copyright 2013 Sebastian Kügler * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2, * or (at your option) any later version. * * This program 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 General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include -#include #include "statusnotifiertest.h" int main(int argc, char **argv) { QApplication app(argc, argv); QCommandLineParser parser; const QString description = QStringLiteral("Statusnotifier test app"); const char version[] = "1.0"; app.setApplicationVersion(version); parser.addVersionOption(); parser.addHelpOption(); parser.setApplicationDescription(description); StatusNotifierTest test; int ex = test.runMain(); app.exec(); return ex; } diff --git a/applets/systemtray/tests/statusnotifier/statusnotifiertest.h b/applets/systemtray/tests/statusnotifier/statusnotifiertest.h index 1f2ce01b7..ebc453ba4 100644 --- a/applets/systemtray/tests/statusnotifier/statusnotifiertest.h +++ b/applets/systemtray/tests/statusnotifier/statusnotifiertest.h @@ -1,63 +1,62 @@ /****************************************************************************** * Copyright 2013 Sebastian Kügler * * * * 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. * *******************************************************************************/ #ifndef STATUSNOTIFIERTEST_H #define STATUSNOTIFIERTEST_H #include -#include #include #include #include "ui_statusnotifiertest.h" class StatusNotifierTestPrivate; class StatusNotifierTest : public QDialog, public Ui_StatusNotifierTest { Q_OBJECT public: StatusNotifierTest(QWidget* parent = 0); virtual ~StatusNotifierTest(); void init(); void log(const QString &msg); public Q_SLOTS: int runMain(); void timeout(); void updateUi(); void updateNotifier(); void activateRequested (bool active, const QPoint &pos); void scrollRequested (int delta, Qt::Orientation orientation); void secondaryActivateRequested (const QPoint &pos); void enableJob(bool enable = true); void setJobProgress(KJob *j, unsigned long v); void result(KJob *job); private: StatusNotifierTestPrivate* d; }; #endif