diff --git a/applets/appmenu/lib/appmenuapplet.cpp b/applets/appmenu/lib/appmenuapplet.cpp index cf2b1f0c7..495e0414b 100644 --- a/applets/appmenu/lib/appmenuapplet.cpp +++ b/applets/appmenu/lib/appmenuapplet.cpp @@ -1,290 +1,296 @@ /* * 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 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); } actionMenu->winId();//create window handle actionMenu->windowHandle()->setTransientParent(ctx->window()); actionMenu->popup(pos); 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) { //dont initialize the currentIndex when another menu is already shown disconnect(oldMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide); oldMenu->hide(); } } setCurrentIndex(idx); // FIXME TODO connect only once connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection); - return; + } else { // is it just an action without a menu? + const QVariant data = m_model->index(idx, 0).data(AppMenuModel::ActionRole); + QAction *action = static_cast(data.value()); + if (action) { + Q_ASSERT(!action->menu()); + action->trigger(); + } } } // 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/package/contents/ui/main.qml b/applets/appmenu/package/contents/ui/main.qml index fb002c2d1..6dc4adf8c 100644 --- a/applets/appmenu/package/contents/ui/main.qml +++ b/applets/appmenu/package/contents/ui/main.qml @@ -1,139 +1,143 @@ /* * Copyright 2013 Heena Mahour * Copyright 2013 Sebastian Kügler * 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) 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, see . */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import org.kde.plasma.plasmoid 2.0 import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.private.appmenu 1.0 as AppMenuPrivate Item { id: root readonly property bool vertical: plasmoid.formFactor === PlasmaCore.Types.Vertical readonly property bool view: plasmoid.configuration.compactView readonly property bool menuAvailable: appMenuModel.menuAvailable readonly property bool kcmAuthorized: KCMShell.authorize(["style.desktop"]).length > 0 onViewChanged: { plasmoid.nativeInterface.view = view } Plasmoid.preferredRepresentation: (plasmoid.configuration.compactView || vertical) ? Plasmoid.compactRepresentation : Plasmoid.fullRepresentation Plasmoid.compactRepresentation: PlasmaComponents.ToolButton { readonly property int fakeIndex: 0 Layout.fillWidth: false Layout.fillHeight: false Layout.minimumWidth: implicitWidth Layout.maximumWidth: implicitWidth enabled: menuAvailable checkable: menuAvailable && plasmoid.nativeInterface.currentIndex === fakeIndex checked: checkable iconSource: i18n("application-menu") onClicked: plasmoid.nativeInterface.trigger(this, 0); } Plasmoid.fullRepresentation: GridLayout { id: buttonGrid Plasmoid.status: { if (menuAvailable && plasmoid.nativeInterface.currentIndex > -1 && buttonRepeater.count > 0) { return PlasmaCore.Types.NeedsAttentionStatus; } else if (menuAvailable){ //when we're not enabled set to active to show the configure button return buttonRepeater.count > 0 ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.HiddenStatus; } else { return PlasmaCore.Types.PassiveStatus; } } Layout.minimumWidth: implicitWidth Layout.minimumHeight: implicitHeight flow: root.vertical ? GridLayout.TopToBottom : GridLayout.LeftToRight rowSpacing: units.smallSpacing columnSpacing: units.smallSpacing Component.onCompleted: { plasmoid.nativeInterface.buttonGrid = buttonGrid // using a Connections {} doesn't work for some reason in Qt >= 5.8 plasmoid.nativeInterface.requestActivateIndex.connect(function (index) { var idx = Math.max(0, Math.min(buttonRepeater.count - 1, index)) var button = buttonRepeater.itemAt(index) if (button) { button.clicked() } }); } // So we can show mnemonic underlines only while Alt is pressed PlasmaCore.DataSource { id: keystateSource engine: "keystate" connectedSources: ["Alt"] } Repeater { id: buttonRepeater model: appMenuModel PlasmaComponents.ToolButton { readonly property int buttonIndex: index Layout.preferredWidth: minimumWidth Layout.fillWidth: root.vertical Layout.fillHeight: !root.vertical text: { var text = activeMenu; var alt = keystateSource.data.Alt; if (!alt || !alt.Pressed) { // StyleHelpers.removeMnemonics text = text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) { return p1.concat(p2, p3); }); } return text; } // fake highlighted checkable: plasmoid.nativeInterface.currentIndex === index checked: checkable visible: text !== "" onClicked: { plasmoid.nativeInterface.trigger(this, index) + + checked = Qt.binding(function() { + return plasmoid.nativeInterface.currentIndex === index; + }); } } } } AppMenuPrivate.AppMenuModel { id: appMenuModel onRequestActivateIndex: plasmoid.nativeInterface.requestActivateIndex(index) Component.onCompleted: { plasmoid.nativeInterface.model = appMenuModel } } }