diff --git a/lib/appmenuapplet.cpp b/lib/appmenuapplet.cpp index 082ead3..ca66df4 100644 --- a/lib/appmenuapplet.cpp +++ b/lib/appmenuapplet.cpp @@ -1,294 +1,323 @@ /* * 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 int AppMenuApplet::s_refs = 0; static const QString s_viewService(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(s_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(s_viewService); - } + unregisterService(); } else { - //if we're the first, regster the service - if (++s_refs == 1) { - QDBusConnection::sessionBus().interface()->registerService(s_viewService, - QDBusConnectionInterface::QueueService, - QDBusConnectionInterface::DontAllowReplacement); - } + registerService(); } }); } AppMenuApplet::~AppMenuApplet() = default; void AppMenuApplet::init() { } AppMenuModel *AppMenuApplet::model() const { return m_model; } +void AppMenuApplet::registerService() +{ + qDebug() << "registering appmenu service"; + ++s_refs; + //if we're the first, regster the service + if (s_refs == 1) { + qDebug() << " -> connecting to DBus"; + QDBusConnection::sessionBus().interface()->registerService(s_viewService, + QDBusConnectionInterface::QueueService, + QDBusConnectionInterface::DontAllowReplacement); + } +} + +void AppMenuApplet::unregisterService() +{ + qDebug() << "unregistering from appmenu service"; + //if we were the last, unregister + if (--s_refs == 0) { + qDebug() << " -> disconnecting from DBus"; + QDBusConnection::sessionBus().interface()->unregisterService(s_viewService); + } + if (s_refs < 0) { + s_refs = 0; + } +} + +bool AppMenuApplet::enabled() const +{ + return m_enabled; +} + +void AppMenuApplet::setEnabled(bool enabled) +{ + if (enabled == m_enabled) { + return; + } + if (enabled) { + registerService(); + } else { + unregisterService(); + } + m_enabled = enabled; +} + 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/lib/appmenuapplet.h b/lib/appmenuapplet.h index 0633c32..ab0f5f2 100644 --- a/lib/appmenuapplet.h +++ b/lib/appmenuapplet.h @@ -1,91 +1,100 @@ /* * 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 . * */ #pragma once #include #include class KDBusMenuImporter; class QQuickItem; class QMenu; class AppMenuModel; class AppMenuApplet : public Plasma::Applet { Q_OBJECT Q_PROPERTY(AppMenuModel* model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(int view READ view WRITE setView NOTIFY viewChanged) Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged) Q_PROPERTY(QQuickItem *buttonGrid READ buttonGrid WRITE setButtonGrid NOTIFY buttonGridChanged) public: enum ViewType { FullView, CompactView }; explicit AppMenuApplet(QObject *parent, const QVariantList &data); ~AppMenuApplet() override; void init() override; int currentIndex() const; QQuickItem *buttonGrid() const; void setButtonGrid(QQuickItem *buttonGrid); AppMenuModel *model() const; void setModel(AppMenuModel *model); + bool enabled() const; + void setEnabled(bool enabled); + int view() const; void setView(int type); + void registerService(); + void unregisterService(); + signals: void modelChanged(); + void enabledChanged(); void viewChanged(); void currentIndexChanged(); void buttonGridChanged(); void requestActivateIndex(int index); public slots: void trigger(QQuickItem *ctx, int idx); protected: - bool eventFilter(QObject *watched, QEvent *event) Q_DECL_OVERRIDE; + bool eventFilter(QObject *watched, QEvent *event) override; private: QMenu *createMenu(int idx) const; void setCurrentIndex(int currentIndex); void onMenuAboutToHide(); - int m_currentIndex = -1; + bool m_enabled = false; int m_viewType = FullView; QPointer m_currentMenu; QPointer m_buttonGrid; QPointer m_model; static int s_refs; }; diff --git a/package/contents/ui/AppMenu.qml b/package/contents/ui/AppMenu.qml index 5413e1f..36a5ba5 100644 --- a/package/contents/ui/AppMenu.qml +++ b/package/contents/ui/AppMenu.qml @@ -1,168 +1,172 @@ import QtQuick 2.2 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.components 2.0 as PlasmaComponents Item { id: appmenu anchors.fill: parent property bool appmenuEnabled: plasmoid.configuration.appmenuEnabled property bool appmenuNextToButtons: plasmoid.configuration.appmenuNextToButtons property bool appmenuFillHeight: plasmoid.configuration.appmenuFillHeight property bool appmenuFontBold: plasmoid.configuration.appmenuFontBold property bool appmenuEnabledAndNonEmpty: appmenuEnabled && appMenuModel !== null && appMenuModel.menuAvailable property bool appmenuOpened: appmenuEnabled && plasmoid.nativeInterface.currentIndex > -1 property var appMenuModel: null property bool appmenuButtonsOffsetEnabled: !buttonsStandalone && appmenuNextToButtons && childrenRect.width > 0 property double appmenuOffsetWidth: visible && appmenuNextToIconAndText && !appmenuSwitchSidesWithIconAndText ? appmenu.childrenRect.width + (appmenuButtonsOffsetEnabled ? controlButtonsArea.width : 0) + appmenuSideMargin*2 : 0 visible: appmenuEnabledAndNonEmpty && !noWindowActive && (appmenuNextToIconAndText || mouseHover || appmenuOpened) GridLayout { id: buttonGrid Layout.minimumWidth: implicitWidth Layout.minimumHeight: implicitHeight flow: GridLayout.LeftToRight rowSpacing: 0 columnSpacing: 0 anchors.top: parent.top anchors.left: parent.left property double placementOffsetButtons: appmenuNextToButtons && controlButtonsArea.visible ? controlButtonsArea.width + appmenuSideMargin : 0 property double placementOffset: appmenuNextToIconAndText && appmenuSwitchSidesWithIconAndText ? activeWindowListView.anchors.leftMargin + windowTitleText.anchors.leftMargin + Math.min(windowTitleText.implicitWidth, windowTitleText.width) + appmenuSideMargin : placementOffsetButtons anchors.leftMargin: (bp === 1 || bp === 3) ? parent.width - width - placementOffset : placementOffset anchors.topMargin: (bp === 2 || bp === 3) ? 0 : parent.height - height Component.onCompleted: { plasmoid.nativeInterface.buttonGrid = buttonGrid + plasmoid.nativeInterface.enabled = appmenuEnabled } Connections { target: plasmoid.nativeInterface onRequestActivateIndex: { var idx = Math.max(0, Math.min(buttonRepeater.count - 1, index)) var button = buttonRepeater.itemAt(index) if (button) { button.clicked(null) } } } Repeater { id: buttonRepeater model: null MouseArea { id: appmenuButton hoverEnabled: true readonly property int buttonIndex: index property bool menuOpened: plasmoid.nativeInterface.currentIndex === index Layout.preferredWidth: appmenuButtonBackground.width Layout.preferredHeight: appmenuButtonBackground.height Rectangle { id: appmenuButtonBackground border.color: 'transparent' width: appmenuButtonTitle.implicitWidth + units.smallSpacing * 3 height: appmenuFillHeight ? appmenu.height : appmenuButtonTitle.implicitHeight + units.smallSpacing color: menuOpened ? theme.highlightColor : 'transparent' radius: units.smallSpacing / 2 } PlasmaComponents.Label { id: appmenuButtonTitle anchors.centerIn: appmenuButtonBackground font.pixelSize: fontPixelSize * plasmoid.configuration.appmenuButtonTextSizeScale text: activeMenu.replace('&', '') font.weight: appmenuFontBold ? Font.Bold : theme.defaultFont.weight } onClicked: { plasmoid.nativeInterface.trigger(this, index) } onEntered: { appmenuButtonBackground.border.color = theme.highlightColor } onExited: { appmenuButtonBackground.border.color = 'transparent' } } } } Rectangle { id: separator anchors.left: buttonGrid.left anchors.leftMargin: appmenuSwitchSidesWithIconAndText ? - appmenuSideMargin * 0.5 : buttonGrid.width + appmenuSideMargin * 0.5 anchors.verticalCenter: buttonGrid.verticalCenter height: 0.8 * parent.height width: 1 visible: appmenuNextToIconAndText && plasmoid.configuration.appmenuSeparatorEnabled color: theme.textColor opacity: 0.4 } function initializeAppModel() { if (appMenuModel !== null) { return } print('initializing appMenuModel...') try { appMenuModel = Qt.createQmlObject( 'import QtQuick 2.2;\ import org.kde.plasma.plasmoid 2.0;\ import org.kde.private.activeWindowControl 1.0 as ActiveWindowControlPrivate;\ ActiveWindowControlPrivate.AppMenuModel {\ id: appMenuModel;\ Component.onCompleted: {\ plasmoid.nativeInterface.model = appMenuModel\ }\ }', main) } catch (e) { print('appMenuModel failed to initialize: ' + e) } print('initializing appmenu...DONE ' + appMenuModel) if (appMenuModel !== null) { resetAppmenuModel() } } function resetAppmenuModel() { if (appmenuEnabled) { initializeAppModel() if (appMenuModel === null) { return } print('setting model in QML: ' + appMenuModel) for (var key in appMenuModel) { print(' ' + key + ' -> ' + appMenuModel[key]) } plasmoid.nativeInterface.model = appMenuModel buttonRepeater.model = appMenuModel } else { plasmoid.nativeInterface.model = null buttonRepeater.model = null } } onAppmenuEnabledChanged: { appmenu.resetAppmenuModel() + if (appMenuModel !== null) { + plasmoid.nativeInterface.enabled = appmenuEnabled + } } } \ No newline at end of file