diff --git a/CMakeLists.txt b/CMakeLists.txt index 29ea2e4..3a7ecc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,26 +1,26 @@ project(simplemenu) -set(PROJECT_VERSION "1.0.7") +set(PROJECT_VERSION "1.0.8") set(PROJECT_VERSION_MAJOR 1) cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR) set(QT_MIN_VERSION "5.4.0") find_package(ECM 0.0.11 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings) include(ECMInstallIcons) include(ECMMarkAsTest) include(ECMMarkNonGuiExecutable) include(ECMOptionalAddSubdirectory) include(FeatureSummary) include(CheckIncludeFiles) set(KF5_MIN_VERSION "5.24.0") find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Plasma ) plasma_install_package(package org.kde.plasma.simplemenu) diff --git a/ChangeLog b/ChangeLog index 5cb8a7f..338a67d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,36 +1,52 @@ + +Changes in v1.0.8: +* Made switching categories by hovering them optional. You can now + turn it off in the settings. +* Custom panel button images now work better. They're smoothly + scaled, and square images are automatically rounded to crisp sizes + just like normal icons are. +* The UI for setting custom button images now uses the regular icon + picker dialog (it supports custom images in recent releases of the + KDE Frameworks libraries) and you can also set a custom button + image by just dropping it on the config area. +* Speed optimizations to make the menu open faster and speed up + switching of categories. +* Fixed hovering a menu item interfering with arrow keyboard navi- + gation on newer versions of Qt. + Changes in v1.0.7: * Fixed arrow navigation through the search results after entering search terms not working. * Improved arrow navigation while searching: Previously, it was necessary to arrow down into the results after entering search terms before horizontal arrow navigation through the search results would work. This was changed, so that e.g. arrow right will now immediately move to the second search result, for a more friction-free search and launch experience. * Tab and Shift+Tab can now be used to cycle through the search field, the system favorites (Shutdown & co) and the category list. Changes in v1.0.6: * Fixed triggering context menu actions. Changes in v1.0.5: * Speed optimizations to open the menu faster. Changes in v1.0.4: * Fixed menu appearing on the wrong monitor in multi-monitor setups. * Fixed crash when opening the context menu for search result items. Changes in v1.0.3: * Fix favorites on Plasma 5.11 and support the new globally shared favorites and unique per-activity favorites. Changes in v1.0.2: * Made sure initial arrow key press always selects first item. * Polished search experience: First search result is selected while searching, no longer requiring extra key presses to launch. * Fixed auto-hide panels not staying open while Simple Menu is open. * Support for the panel icon size hint in Frameworks 5, prevents panel icon getting too large in wide panels. * Fixed panel icon missing mouseover highlight effect. * Now requiring KDE Frameworks v5.34. diff --git a/package/contents/config/main.xml b/package/contents/config/main.xml index 88efe70..9e1c39d 100644 --- a/package/contents/config/main.xml +++ b/package/contents/config/main.xml @@ -1,59 +1,63 @@ start-here-kde false true 0 false preferred://browser,kontact.desktop,systemsettings.desktop,org.kde.dolphin.desktop,ktp-contactlist.desktop,org.kde.kate.desktop logout,lock-screen,reboot,shutdown true bookmarks,baloosearch true + + + true + muon-discover --application false diff --git a/package/contents/ui/CompactRepresentation.qml b/package/contents/ui/CompactRepresentation.qml index c7202ca..9777c70 100644 --- a/package/contents/ui/CompactRepresentation.qml +++ b/package/contents/ui/CompactRepresentation.qml @@ -1,109 +1,113 @@ /*************************************************************************** * Copyright (C) 2013-2014 by Eike Hein * * * * 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 . * ***************************************************************************/ import QtQuick 2.0 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore Item { id: root readonly property var screenGeometry: plasmoid.screenGeometry readonly property bool inPanel: (plasmoid.location == PlasmaCore.Types.TopEdge || plasmoid.location == PlasmaCore.Types.RightEdge || plasmoid.location == PlasmaCore.Types.BottomEdge || plasmoid.location == PlasmaCore.Types.LeftEdge) readonly property bool vertical: (plasmoid.formFactor == PlasmaCore.Types.Vertical) readonly property bool useCustomButtonImage: (plasmoid.configuration.useCustomButtonImage && plasmoid.configuration.customButtonImage.length != 0) property QtObject dashWindow: null Plasmoid.status: dashWindow && dashWindow.visible ? PlasmaCore.Types.RequiresAttentionStatus : PlasmaCore.Types.PassiveStatus onWidthChanged: updateSizeHints() onHeightChanged: updateSizeHints() function updateSizeHints() { if (useCustomButtonImage) { if (vertical) { var scaledHeight = Math.floor(parent.width * (buttonIcon.implicitHeight / buttonIcon.implicitWidth)); root.Layout.minimumHeight = scaledHeight; root.Layout.maximumHeight = scaledHeight; root.Layout.minimumWidth = units.iconSizes.small; root.Layout.maximumWidth = inPanel ? units.iconSizeHints.panel : -1; } else { var scaledWidth = Math.floor(parent.height * (buttonIcon.implicitWidth / buttonIcon.implicitHeight)); root.Layout.minimumWidth = scaledWidth; root.Layout.maximumWidth = scaledWidth; root.Layout.minimumHeight = units.iconSizes.small; root.Layout.maximumHeight = inPanel ? units.iconSizeHints.panel : -1; } } else { root.Layout.minimumWidth = units.iconSizes.small; root.Layout.maximumWidth = inPanel ? units.iconSizeHints.panel : -1; root.Layout.minimumHeight = units.iconSizes.small root.Layout.maximumHeight = inPanel ? units.iconSizeHints.panel : -1; } } Connections { target: units.iconSizeHints onPanelChanged: updateSizeHints() } PlasmaCore.IconItem { id: buttonIcon anchors.fill: parent readonly property double aspectRatio: (vertical ? implicitHeight / implicitWidth : implicitWidth / implicitHeight) source: useCustomButtonImage ? plasmoid.configuration.customButtonImage : plasmoid.configuration.icon active: mouseArea.containsMouse - roundToIconSize: !useCustomButtonImage + smooth: true + + // A custom icon could also be rectangular. However, if a square, custom, icon is given, assume it + // to be an icon and round it to the nearest icon size again to avoid scaling artefacts. + roundToIconSize: !useCustomButtonImage || aspectRatio === 1 onSourceChanged: updateSizeHints() } MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true onClicked: { dashWindow.visible = !dashWindow.visible; } } Component.onCompleted: { dashWindow = Qt.createQmlObject("MenuRepresentation {}", root); plasmoid.activated.connect(function() { dashWindow.visible = !dashWindow.visible; }); } } diff --git a/package/contents/ui/ConfigGeneral.qml b/package/contents/ui/ConfigGeneral.qml index b72fe9b..a6a356d 100644 --- a/package/contents/ui/ConfigGeneral.qml +++ b/package/contents/ui/ConfigGeneral.qml @@ -1,135 +1,202 @@ /*************************************************************************** * Copyright (C) 2014 by Eike Hein * * * * 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 . * ***************************************************************************/ import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents + +import org.kde.kquickcontrolsaddons 2.0 as KQuickAddons +import org.kde.draganddrop 2.0 as DragDrop + import org.kde.plasma.private.kicker 0.1 as Kicker Item { id: configGeneral width: childrenRect.width height: childrenRect.height - property alias cfg_useCustomButtonImage: useCustomButtonImage.checked - property alias cfg_customButtonImage: customButtonImage.text + property string cfg_icon: plasmoid.configuration.icon + property bool cfg_useCustomButtonImage: plasmoid.configuration.useCustomButtonImage + property string cfg_customButtonImage: plasmoid.configuration.customButtonImage property alias cfg_appNameFormat: appNameFormat.currentIndex + property alias cfg_switchCategoriesOnHover: switchCategoriesOnHover.checked property alias cfg_useExtraRunners: useExtraRunners.checked ColumnLayout { - GroupBox { - Layout.fillWidth: true + anchors.left: parent.left - title: i18n("Icon") + RowLayout { + spacing: units.smallSpacing - flat: true - - RowLayout { - CheckBox { - id: useCustomButtonImage + Label { + text: i18n("Icon:") + } - text: i18n("Use custom image:") - } + Button { + id: iconButton + Layout.minimumWidth: previewFrame.width + units.smallSpacing * 2 + Layout.maximumWidth: Layout.minimumWidth + Layout.minimumHeight: previewFrame.height + units.smallSpacing * 2 + Layout.maximumHeight: Layout.minimumWidth - TextField { - id: customButtonImage + DragDrop.DropArea { + id: dropArea - enabled: useCustomButtonImage.checked + property bool containsAcceptableDrag: false - Layout.fillWidth: true - } + anchors.fill: parent - Button { - iconName: "document-open" + onDragEnter: { + // Cannot use string operations (e.g. indexOf()) on "url" basic type. + var urlString = event.mimeData.url.toString(); - enabled: useCustomButtonImage.checked + // This list is also hardcoded in KIconDialog. + var extensions = [".png", ".xpm", ".svg", ".svgz"]; + containsAcceptableDrag = urlString.indexOf("file:///") === 0 && extensions.some(function (extension) { + return urlString.indexOf(extension) === urlString.length - extension.length; // "endsWith" + }); - onClicked: { - imagePicker.folder = systemSettings.picturesLocation(); - imagePicker.open(); + if (!containsAcceptableDrag) { + event.ignore(); + } + } + onDragLeave: containsAcceptableDrag = false + + onDrop: { + if (containsAcceptableDrag) { + // Strip file:// prefix, we already verified in onDragEnter that we have only local URLs. + iconDialog.setCustomButtonImage(event.mimeData.url.toString().substr("file://".length)); + } + containsAcceptableDrag = false; } } - FileDialog { - id: imagePicker + KQuickAddons.IconDialog { + id: iconDialog - title: i18n("Choose an image") + function setCustomButtonImage(image) { + cfg_customButtonImage = image || cfg_icon || "start-here-kde" + cfg_useCustomButtonImage = true; + } - selectFolder: false - selectMultiple: false + onIconNameChanged: setCustomButtonImage(iconName); + } + + // just to provide some visual feedback, cannot have checked without checkable enabled + checkable: true + checked: dropArea.containsAcceptableDrag + onClicked: { + checked = Qt.binding(function() { // never actually allow it being checked + return iconMenu.status === PlasmaComponents.DialogStatus.Open || dropArea.containsAcceptableDrag; + }) - nameFilters: [ i18n("Image Files (*.png *.jpg *.jpeg *.bmp *.svg *.svgz)") ] + iconMenu.open(0, height) + } - onFileUrlChanged: { - customButtonImage.text = fileUrl; + PlasmaCore.FrameSvgItem { + id: previewFrame + anchors.centerIn: parent + imagePath: plasmoid.location === PlasmaCore.Types.Vertical || plasmoid.location === PlasmaCore.Types.Horizontal + ? "widgets/panel-background" : "widgets/background" + width: units.iconSizes.large + fixedMargins.left + fixedMargins.right + height: units.iconSizes.large + fixedMargins.top + fixedMargins.bottom + + PlasmaCore.IconItem { + anchors.centerIn: parent + width: units.iconSizes.large + height: width + source: cfg_useCustomButtonImage ? cfg_customButtonImage : cfg_icon } } + } + + // QQC Menu can only be opened at cursor position, not a random one + PlasmaComponents.ContextMenu { + id: iconMenu + visualParent: iconButton - Kicker.SystemSettings { - id: systemSettings + PlasmaComponents.MenuItem { + text: i18nc("@item:inmenu Open icon chooser dialog", "Choose...") + icon: "document-open-folder" + onClicked: iconDialog.open() + } + PlasmaComponents.MenuItem { + text: i18nc("@item:inmenu Reset icon to default", "Clear Icon") + icon: "edit-clear" + onClicked: { + cfg_useCustomButtonImage = false; + } } } } GroupBox { Layout.fillWidth: true title: i18n("Behavior") flat: true ColumnLayout { RowLayout { Label { text: i18n("Show applications as:") } ComboBox { id: appNameFormat Layout.fillWidth: true model: [i18n("Name only"), i18n("Description only"), i18n("Name (Description)"), i18n("Description (Name)")] } } + + CheckBox { + id: switchCategoriesOnHover + + text: i18n("Switch categories on hover") + } } } GroupBox { Layout.fillWidth: true title: i18n("Search") flat: true ColumnLayout { CheckBox { id: useExtraRunners text: i18n("Expand search to bookmarks, files and emails") } } } } } diff --git a/package/contents/ui/MenuRepresentation.qml b/package/contents/ui/MenuRepresentation.qml index a5de09f..bbf1c9b 100644 --- a/package/contents/ui/MenuRepresentation.qml +++ b/package/contents/ui/MenuRepresentation.qml @@ -1,793 +1,803 @@ /*************************************************************************** * Copyright (C) 2014 by Weng Xuetian * Copyright (C) 2013-2017 by Eike Hein * * * * 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 . * ***************************************************************************/ import QtQuick 2.4 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.plasma.private.kicker 0.1 as Kicker PlasmaCore.Dialog { id: root objectName: "popupWindow" flags: Qt.WindowStaysOnTopHint location: PlasmaCore.Types.Floating hideOnWindowDeactivate: true property int iconSize: units.iconSizes.huge property int cellSize: iconSize + theme.mSize(theme.defaultFont).height + (2 * units.smallSpacing) + (2 * Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom, highlightItemSvg.margins.left + highlightItemSvg.margins.right)) property bool searching: (searchField.text != "") onVisibleChanged: { if (!visible) { reset(); } else { var pos = popupPosition(width, height); x = pos.x; y = pos.y; requestActivate(); } } onHeightChanged: { var pos = popupPosition(width, height); x = pos.x; y = pos.y; } onWidthChanged: { var pos = popupPosition(width, height); x = pos.x; y = pos.y; } onSearchingChanged: { if (searching) { pageList.model = runnerModel; paginationBar.model = runnerModel; } else { reset(); } } function reset() { if (!searching) { if (filterListScrollArea.visible) { filterList.currentIndex = 0; } else { pageList.model = rootModel.modelForRow(0); paginationBar.model = rootModel.modelForRow(0); } } searchField.text = ""; pageListScrollArea.focus = true; pageList.currentIndex = 0; pageList.currentItem.itemGrid.currentIndex = -1; } function popupPosition(width, height) { var screenAvail = plasmoid.availableScreenRect; var screenGeom = plasmoid.screenGeometry; //QtBug - QTBUG-64115 var screen = Qt.rect(screenAvail.x + screenGeom.x, screenAvail.y + screenGeom.y, screenAvail.width, screenAvail.height); var offset = units.gridUnit; // Fall back to bottom-left of screen area when the applet is on the desktop or floating. var x = offset; var y = screen.height - height - offset; if (plasmoid.location == PlasmaCore.Types.BottomEdge) { var horizMidPoint = screen.x + (screen.width / 2); var appletTopLeft = parent.mapToGlobal(0, 0); x = (appletTopLeft.x < horizMidPoint) ? screen.x + offset : (screen.x + screen.width) - width - offset; y = screen.height - height - offset - panelSvg.margins.top; } else if (plasmoid.location == PlasmaCore.Types.TopEdge) { var horizMidPoint = screen.x + (screen.width / 2); var appletBottomLeft = parent.mapToGlobal(0, parent.height); x = (appletBottomLeft.x < horizMidPoint) ? screen.x + offset : (screen.x + screen.width) - width - offset; y = parent.height + panelSvg.margins.bottom + offset; } else if (plasmoid.location == PlasmaCore.Types.LeftEdge) { var vertMidPoint = screen.y + (screen.height / 2); var appletTopLeft = parent.mapToGlobal(0, 0); x = parent.width + panelSvg.margins.right + offset; y = (appletTopLeft.y < vertMidPoint) ? screen.y + offset : (screen.y + screen.height) - height - offset; } else if (plasmoid.location == PlasmaCore.Types.RightEdge) { var vertMidPoint = screen.y + (screen.height / 2); var appletTopLeft = parent.mapToGlobal(0, 0); x = appletTopLeft.x - panelSvg.margins.left - offset - width; y = (appletTopLeft.y < vertMidPoint) ? screen.y + offset : (screen.y + screen.height) - height - offset; } return Qt.point(x, y); } FocusScope { Layout.minimumWidth: (cellSize * 6) + Math.max(systemFavoritesGrid.width, filterListScrollArea.width) + units.smallSpacing Layout.maximumWidth: (cellSize * 6) + Math.max(systemFavoritesGrid.width, filterListScrollArea.width) + units.smallSpacing Layout.minimumHeight: (cellSize * 4) + searchField.height + paginationBar.height + (2 * units.smallSpacing) Layout.maximumHeight: (cellSize * 4) + searchField.height + paginationBar.height + (2 * units.smallSpacing) focus: true PlasmaExtras.Heading { id: dummyHeading visible: false width: 0 level: 5 } TextMetrics { id: headingMetrics font: dummyHeading.font } ActionMenu { id: actionMenu onActionClicked: visualParent.actionTriggered(actionId, actionArgument) onClosed: { if (pageList.currentItem) { pageList.currentItem.itemGrid.currentIndex = -1; } } } PlasmaComponents.TextField { id: searchField anchors.top: parent.top anchors.left: parent.left anchors.right: systemFavoritesGrid.left anchors.rightMargin: units.smallSpacing width: parent.width placeholderText: i18n("Search...") clearButtonShown: true onTextChanged: { runnerModel.query = text; } Keys.onPressed: { if (event.key == Qt.Key_Down) { event.accepted = true; pageList.currentItem.itemGrid.tryActivate(0, 0); } else if (event.key == Qt.Key_Right) { if (cursorPosition == length) { event.accepted = true; if (pageList.currentItem.itemGrid.currentIndex == -1) { pageList.currentItem.itemGrid.tryActivate(0, 0); } else { pageList.currentItem.itemGrid.tryActivate(0, 1); } } } else if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter) { if (text != "" && pageList.currentItem.itemGrid.count > 0) { event.accepted = true; pageList.currentItem.itemGrid.tryActivate(0, 0); pageList.currentItem.itemGrid.model.trigger(0, "", null); root.visible = false; } } else if (event.key == Qt.Key_Tab) { event.accepted = true; systemFavoritesGrid.tryActivate(0, 0); } else if (event.key == Qt.Key_Backtab) { event.accepted = true; if (!searching) { filterList.forceActiveFocus(); } else { systemFavoritesGrid.tryActivate(0, 0); } } } function backspace() { if (!root.visible) { return; } focus = true; text = text.slice(0, -1); } function appendText(newText) { if (!root.visible) { return; } focus = true; text = text + newText; } } ItemGridView { id: systemFavoritesGrid anchors { top: parent.top right: parent.right } width: cellWidth * 4 height: searchField.height cellWidth: height cellHeight: height iconSize: height - units.smallSpacing horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff dragEnabled: true showLabels: false model: systemFavorites onCurrentIndexChanged: { focus = true; } onKeyNavLeft: { currentIndex = -1; searchField.focus = true; } onKeyNavDown: { pageListScrollArea.focus = true; if (pageList.currentItem) { pageList.currentItem.itemGrid.tryActivate(0, 0); } } Keys.onPressed: { if (event.key == Qt.Key_Tab) { event.accepted = true; currentIndex = -1; if (!searching) { filterList.forceActiveFocus(); } else { searchField.focus = true; } } else if (event.key == Qt.Key_Backtab) { event.accepted = true; currentIndex = -1; searchField.focus = true; } } } PlasmaExtras.ScrollArea { id: pageListScrollArea anchors { left: parent.left top: searchField.bottom topMargin: units.smallSpacing bottom: paginationBar.top bottomMargin: units.smallSpacing } width: (cellSize * 6) focus: true horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff ListView { id: pageList anchors.fill: parent orientation: Qt.Horizontal snapMode: ListView.SnapOneItem currentIndex: 0 model: rootModel.modelForRow(0) onCurrentIndexChanged: { positionViewAtIndex(currentIndex, ListView.Contain); } onCurrentItemChanged: { if (!currentItem) { return; } currentItem.itemGrid.focus = true; } onModelChanged: { currentIndex = 0; } onFlickingChanged: { if (!flicking) { var pos = mapToItem(contentItem, root.width / 2, root.height / 2); var itemIndex = indexAt(pos.x, pos.y); currentIndex = itemIndex; } } function cycle() { enabled = false; enabled = true; } function activateNextPrev(next) { if (next) { var newIndex = pageList.currentIndex + 1; if (newIndex == pageList.count) { newIndex = 0; } pageList.currentIndex = newIndex; } else { var newIndex = pageList.currentIndex - 1; if (newIndex < 0) { newIndex = (pageList.count - 1); } pageList.currentIndex = newIndex; } } delegate: Item { width: cellSize * 6 height: cellSize * 4 property Item itemGrid: gridView ItemGridView { id: gridView anchors.fill: parent cellWidth: cellSize cellHeight: cellSize horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff dragEnabled: (index == 0) model: searching ? runnerModel.modelForRow(index) : rootModel.modelForRow(filterListScrollArea.visible ? filterList.currentIndex : 0).modelForRow(index) onCurrentIndexChanged: { if (currentIndex != -1 && !searching) { pageListScrollArea.focus = true; focus = true; } } onCountChanged: { if (searching && index == 0) { currentIndex = 0; } } onKeyNavUp: { currentIndex = -1; searchField.focus = true; } onKeyNavRight: { var newIndex = pageList.currentIndex + 1; var cRow = currentRow(); if (newIndex == pageList.count) { newIndex = 0; } pageList.currentIndex = newIndex; pageList.currentItem.itemGrid.tryActivate(cRow, 0); } onKeyNavLeft: { var newIndex = pageList.currentIndex - 1; var cRow = currentRow(); if (newIndex < 0) { newIndex = (pageList.count - 1); } pageList.currentIndex = newIndex; pageList.currentItem.itemGrid.tryActivate(cRow, 5); } } Kicker.WheelInterceptor { anchors.fill: parent z: 1 property int wheelDelta: 0 function scrollByWheel(wheelDelta, eventDelta) { // magic number 120 for common "one click" // See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop wheelDelta += eventDelta; var increment = 0; while (wheelDelta >= 120) { wheelDelta -= 120; increment++; } while (wheelDelta <= -120) { wheelDelta += 120; increment--; } while (increment != 0) { pageList.activateNextPrev(increment < 0); increment += (increment < 0) ? 1 : -1; } return wheelDelta; } onWheelMoved: { wheelDelta = scrollByWheel(wheelDelta, delta.y); } } } } } ListView { id: paginationBar anchors { bottom: parent.bottom horizontalCenter: parent.horizontalCenter } width: model.count * units.iconSizes.small height: units.iconSizes.small orientation: Qt.Horizontal model: rootModel.modelForRow(0) delegate: Item { width: units.iconSizes.small height: width Rectangle { id: pageDelegate anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } width: parent.width / 2 height: width property bool isCurrent: (pageList.currentIndex == index) radius: width / 2 color: theme.textColor opacity: 0.5 Behavior on width { SmoothedAnimation { duration: units.longDuration; velocity: 0.01 } } Behavior on opacity { SmoothedAnimation { duration: units.longDuration; velocity: 0.01 } } states: [ State { when: pageDelegate.isCurrent PropertyChanges { target: pageDelegate; width: parent.width - (units.smallSpacing * 2) } PropertyChanges { target: pageDelegate; opacity: 0.8 } } ] } MouseArea { anchors.fill: parent onClicked: pageList.currentIndex = index; property int wheelDelta: 0 function scrollByWheel(wheelDelta, eventDelta) { // magic number 120 for common "one click" // See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop wheelDelta += eventDelta; var increment = 0; while (wheelDelta >= 120) { wheelDelta -= 120; increment++; } while (wheelDelta <= -120) { wheelDelta += 120; increment--; } while (increment != 0) { pageList.activateNextPrev(increment < 0); increment += (increment < 0) ? 1 : -1; } return wheelDelta; } onWheel: { wheelDelta = scrollByWheel(wheelDelta, wheel.angleDelta.y); } } } } PlasmaExtras.ScrollArea { id: filterListScrollArea anchors { left: pageListScrollArea.right leftMargin: units.smallSpacing top: searchField.bottom topMargin: units.smallSpacing bottom: paginationBar.top bottomMargin: units.smallSpacing } property int desiredWidth: 0 width: plasmoid.configuration.showFilterList ? desiredWidth : 0 enabled: !searching visible: plasmoid.configuration.showFilterList property alias currentIndex: filterList.currentIndex opacity: root.visible ? (searching ? 0.30 : 1.0) : 0.3 Behavior on opacity { SmoothedAnimation { duration: units.longDuration; velocity: 0.01 } } verticalScrollBarPolicy: (opacity == 1.0) ? Qt.ScrollBarAsNeeded : Qt.ScrollBarAlwaysOff onEnabledChanged: { if (!enabled) { filterList.currentIndex = -1; } } ListView { id: filterList focus: true property bool allApps: false property int eligibleWidth: width property int hItemMargins: highlightItemSvg.margins.left + highlightItemSvg.margins.right model: filterListScrollArea.visible ? rootModel : null boundsBehavior: Flickable.StopAtBounds snapMode: ListView.SnapToItem spacing: 0 keyNavigationWraps: true delegate: MouseArea { id: item property int textWidth: label.contentWidth property int mouseCol width: parent.width height: label.paintedHeight + highlightItemSvg.margins.top + highlightItemSvg.margins.bottom Accessible.role: Accessible.MenuItem Accessible.name: model.display acceptedButtons: Qt.LeftButton hoverEnabled: true onContainsMouseChanged: { if (!containsMouse) { updateCurrentItemTimer.stop(); } } + onPressed: { + if (!plasmoid.configuration.switchCategoriesOnHover) { + ListView.view.currentIndex = index; + } + } + onPositionChanged: { // Lazy menu implementation. + if (!plasmoid.configuration.switchCategoriesOnHover) { + return; + } + mouseCol = mouse.x; if (index == ListView.view.currentIndex) { updateCurrentItem(); } else if ((index == ListView.view.currentIndex - 1) && mouse.y < (item.height - 6) || (index == ListView.view.currentIndex + 1) && mouse.y > 5) { if (mouse.x > ListView.view.eligibleWidth - 5) { updateCurrentItem(); } } else if (mouse.x > ListView.view.eligibleWidth) { updateCurrentItem(); } updateCurrentItemTimer.start(); } function updateCurrentItem() { ListView.view.currentIndex = index; ListView.view.eligibleWidth = Math.min(width, mouseCol); } Timer { id: updateCurrentItemTimer interval: 50 repeat: false onTriggered: parent.updateCurrentItem() } PlasmaExtras.Heading { id: label anchors { fill: parent leftMargin: highlightItemSvg.margins.left rightMargin: highlightItemSvg.margins.right } elide: Text.ElideRight wrapMode: Text.NoWrap opacity: 1.0 level: 5 text: model.display } } highlight: PlasmaComponents.Highlight { anchors { top: filterList.currentItem ? filterList.currentItem.top : undefined left: filterList.currentItem ? filterList.currentItem.left : undefined bottom: filterList.currentItem ? filterList.currentItem.bottom : undefined } opacity: filterListScrollArea.focus ? 1.0 : 0.7 width: (highlightItemSvg.margins.left + filterList.currentItem.textWidth + highlightItemSvg.margins.right + units.smallSpacing) visible: filterList.currentItem } highlightFollowsCurrentItem: false highlightMoveDuration: 0 highlightResizeDuration: 0 onCurrentIndexChanged: applyFilter() onCountChanged: { var width = 0; for (var i = 0; i < rootModel.count; ++i) { headingMetrics.text = rootModel.labelForRow(i); if (headingMetrics.width > width) { width = headingMetrics.width; } } filterListScrollArea.desiredWidth = width + hItemMargins + units.gridUnit; } function applyFilter() { if (filterListScrollArea.visible && !searching && currentIndex >= 0) { pageList.model = rootModel.modelForRow(currentIndex); paginationBar.model = pageList.model; } } Keys.onPressed: { if (event.key == Qt.Key_left) { event.accepted = true; var currentRow = Math.max(0, Math.ceil(currentItem.y / cellSize) - 1); if (pageList.currentItem) { pageList.currentItem.itemGrid.tryActivate(currentRow, 5); } } else if (event.key == Qt.Key_Tab) { event.accepted = true; searchField.focus = true; } else if (event.key == Qt.Key_Backtab) { event.accepted = true; systemFavoritesGrid.tryActivate(0, 0); } } } } Keys.onPressed: { if (event.key == Qt.Key_Escape) { event.accepted = true; if (searching) { reset(); } else { root.visible = false; } return; } if (searchField.focus) { return; } if (event.key == Qt.Key_Backspace) { event.accepted = true; searchField.backspace(); } else if (event.key == Qt.Key_Tab || event.key == Qt.Key_Backtab) { if (pageListScrollArea.focus == true && pageList.currentItem.itemGrid.currentIndex == -1) { event.accepted = true; pageList.currentItem.itemGrid.tryActivate(0, 0); } } else if (event.text != "") { event.accepted = true; searchField.appendText(event.text); } } } Component.onCompleted: { kicker.reset.connect(reset); dragHelper.dropped.connect(pageList.cycle); } } diff --git a/package/metadata.desktop b/package/metadata.desktop index d01ad56..59ff85e 100644 --- a/package/metadata.desktop +++ b/package/metadata.desktop @@ -1,20 +1,20 @@ [Desktop Entry] Name=Simple Menu Comment=A simple launcher menu Type=Service Icon=start-here-kde X-KDE-ServiceTypes=Plasma/Applet X-Plasma-API=declarativeappletscript X-Plasma-MainScript=ui/main.qml X-Plasma-Provides=org.kde.plasma.launchermenu X-KDE-PluginInfo-Author=Eike Hein X-KDE-PluginInfo-Email=hein@kde.org X-KDE-PluginInfo-Name=org.kde.plasma.simplemenu -X-KDE-PluginInfo-Version=1.0.7 +X-KDE-PluginInfo-Version=1.0.8 X-KDE-PluginInfo-Category=Application Launchers X-KDE-PluginInfo-Depends= X-KDE-PluginInfo-License=GPL v2+ X-KDE-PluginInfo-EnabledByDefault=true