diff --git a/CMakeLists.txt b/CMakeLists.txt index 2747e46..1d852c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,26 +1,26 @@ project(simplemenu) -set(PROJECT_VERSION "1.0.10") +set(PROJECT_VERSION "1.0.11") 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 e2c4b67..706cc09 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,58 +1,65 @@ +Changes in v1.0.11: +* Launching apps by touchscreen tap now works. +* An item's highlight decoration now stays in place when opening + its context menu. +* Resource usage optimizations for an even faster and lighter menu. +* Minor bugfixes. + Changes in v1.0.10: * Fixed "Add to Favorites" not working as of v1.0.8. Changes in v1.0.9: * Fixed a bug causing system action buttons to stay highlighted when the mouse pointer leaves the area. 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/code/tools.js b/package/contents/code/tools.js index 5266f3f..ffc44d3 100644 --- a/package/contents/code/tools.js +++ b/package/contents/code/tools.js @@ -1,229 +1,231 @@ /*************************************************************************** * Copyright (C) 2013 by Aurélien Gâteau * * Copyright (C) 2013-2015 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 . * ***************************************************************************/ -function fillActionMenu(actionMenu, actionList, favoriteModel, favoriteId) { +.pragma library + +function fillActionMenu(i18n, actionMenu, actionList, favoriteModel, favoriteId) { // Accessing actionList can be a costly operation, so we don't // access it until we need the menu. - var actions = createFavoriteActions(favoriteModel, favoriteId); + var actions = createFavoriteActions(i18n, favoriteModel, favoriteId); if (actions) { if (actionList && actionList.length > 0) { var separator = { "type": "separator" }; actionList.unshift(separator); // actionList = actions.concat(actionList); // this crashes Qt O.o actionList.unshift.apply(actionList, actions); } else { actionList = actions; } } actionMenu.actionList = actionList; } -function createFavoriteActions(favoriteModel, favoriteId) { +function createFavoriteActions(i18n, favoriteModel, favoriteId) { if (favoriteModel === null || !favoriteModel.enabled || favoriteId == null) { return null; } if ("initForClient" in favoriteModel) { var activities = favoriteModel.activities.runningActivities; if (activities.length <= 1) { var action = {}; if (favoriteModel.isFavorite(favoriteId)) { action.text = i18n("Remove from Favorites"); action.icon = "list-remove"; action.actionId = "_kicker_favorite_remove"; } else if (favoriteModel.maxFavorites == -1 || favoriteModel.count < favoriteModel.maxFavorites) { action.text = i18n("Add to Favorites"); action.icon = "bookmark-new"; action.actionId = "_kicker_favorite_add"; } else { return null; } action.actionArgument = { favoriteModel: favoriteModel, favoriteId: favoriteId }; return [action]; } else { var actions = []; var linkedActivities = favoriteModel.linkedActivitiesFor(favoriteId); // Adding the item to link/unlink to all activities var linkedToAllActivities = !(linkedActivities.indexOf(":global") === -1); actions.push({ text : i18n("On All Activities"), checkable : true, actionId : linkedToAllActivities ? "_kicker_favorite_remove_from_activity" : "_kicker_favorite_set_to_activity", checked : linkedToAllActivities, actionArgument : { favoriteModel: favoriteModel, favoriteId: favoriteId, favoriteActivity: "" } }); // Adding items for each activity separately var addActivityItem = function(activityId, activityName) { var linkedToThisActivity = !(linkedActivities.indexOf(activityId) === -1); actions.push({ text : activityName, checkable : true, checked : linkedToThisActivity && !linkedToAllActivities, actionId : // If we are on all activities, and the user clicks just one // specific activity, unlink from everything else linkedToAllActivities ? "_kicker_favorite_set_to_activity" : // If we are linked to the current activity, just unlink from // that single one linkedToThisActivity ? "_kicker_favorite_remove_from_activity" : // Otherwise, link to this activity, but do not unlink from // other ones "_kicker_favorite_add_to_activity", actionArgument : { favoriteModel : favoriteModel, favoriteId : favoriteId, favoriteActivity : activityId } }); }; // Adding the item to link/unlink to the current activity addActivityItem(favoriteModel.activities.currentActivity, i18n("On The Current Activity")); actions.push({ type: "separator", actionId: "_kicker_favorite_separator" }); // Adding the items for each activity activities.forEach(function(activityId) { addActivityItem(activityId, favoriteModel.activityNameForId(activityId)); }); return [{ text : i18n("Show In Favorites"), icon : "favorite", subActions : actions }]; } } else { var action = {}; if (favoriteModel.isFavorite(favoriteId)) { action.text = i18n("Remove from Favorites"); action.icon = "list-remove"; action.actionId = "_kicker_favorite_remove"; } else if (favoriteModel.maxFavorites == -1 || favoriteModel.count < favoriteModel.maxFavorites) { action.text = i18n("Add to Favorites"); action.icon = "bookmark-new"; action.actionId = "_kicker_favorite_add"; } else { return null; } action.actionArgument = { favoriteModel: favoriteModel, favoriteId: favoriteId }; return [action]; } } -function triggerAction(model, index, actionId, actionArgument) { +function triggerAction(plasmoid, model, index, actionId, actionArgument) { function startsWith(txt, needle) { return txt.substr(0, needle.length) === needle; } if (startsWith(actionId, "_kicker_favorite_")) { handleFavoriteAction(actionId, actionArgument); return; } var closeRequested = model.trigger(index, actionId, actionArgument); if (closeRequested) { plasmoid.expanded = false; return true; } return false; } function handleFavoriteAction(actionId, actionArgument) { var favoriteId = actionArgument.favoriteId; var favoriteModel = actionArgument.favoriteModel; console.log(actionId); if (favoriteModel === null || favoriteId == null) { return null; } if ("initForClient" in favoriteModel) { if (actionId == "_kicker_favorite_remove") { console.log("Removing from all activities"); favoriteModel.removeFavoriteFrom(favoriteId, ":any"); } else if (actionId == "_kicker_favorite_add") { console.log("Adding to global activity"); favoriteModel.addFavoriteTo(favoriteId, ":global"); } else if (actionId == "_kicker_favorite_remove_from_activity") { console.log("Removing from a specific activity"); favoriteModel.removeFavoriteFrom(favoriteId, actionArgument.favoriteActivity); } else if (actionId == "_kicker_favorite_add_to_activity") { console.log("Adding to another activity"); favoriteModel.addFavoriteTo(favoriteId, actionArgument.favoriteActivity); } else if (actionId == "_kicker_favorite_set_to_activity") { console.log("Removing the item from the favourites, and re-adding it just to be on a specific activity"); favoriteModel.setFavoriteOn(favoriteId, actionArgument.favoriteActivity); } } else { if (actionId == "_kicker_favorite_remove") { favoriteModel.removeFavorite(favoriteId); } else if (actionId == "_kicker_favorite_add") { favoriteModel.addFavorite(favoriteId); } } } diff --git a/package/contents/ui/ItemGridDelegate.qml b/package/contents/ui/ItemGridDelegate.qml index cb000e8..556e77b 100644 --- a/package/contents/ui/ItemGridDelegate.qml +++ b/package/contents/ui/ItemGridDelegate.qml @@ -1,115 +1,111 @@ /*************************************************************************** * Copyright (C) 2015 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 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.kquickcontrolsaddons 2.0 import "../code/tools.js" as Tools Item { id: item width: GridView.view.cellWidth height: width property bool showLabel: true - property int itemIndex: model.index - property url url: model.url != undefined ? model.url : "" + readonly property int itemIndex: model.index + readonly property url url: model.url != undefined ? model.url : "" property bool pressed: false - property bool hasActionList: ((model.favoriteId != null) + readonly property bool hasActionList: ((model.favoriteId != null) || (("hasActionList" in model) && (model.hasActionList == true))) - property Item view: GridView.view Accessible.role: Accessible.MenuItem Accessible.name: model.display function openActionMenu(x, y) { var actionList = hasActionList ? model.actionList : []; - Tools.fillActionMenu(actionMenu, actionList, GridView.view.model.favoritesModel, model.favoriteId); + Tools.fillActionMenu(i18n, actionMenu, actionList, GridView.view.model.favoritesModel, model.favoriteId); actionMenu.visualParent = item; actionMenu.open(x, y); } function actionTriggered(actionId, actionArgument) { - var close = Tools.triggerAction(GridView.view.model, model.index, actionId, actionArgument); - - if (close) { - root.toggle(); - } + Tools.triggerAction(plasmoid, GridView.view.model, model.index, actionId, actionArgument); } PlasmaCore.IconItem { id: icon y: showLabel ? (2 * highlightItemSvg.margins.top) : 0 anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: showLabel ? undefined : parent.verticalCenter width: iconSize height: width animated: false - usesPlasmaTheme: view.usesPlasmaTheme + usesPlasmaTheme: item.GridView.view.usesPlasmaTheme source: model.decoration } PlasmaComponents.Label { id: label visible: showLabel anchors { top: icon.bottom topMargin: units.smallSpacing left: parent.left leftMargin: highlightItemSvg.margins.left right: parent.right rightMargin: highlightItemSvg.margins.right } horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight wrapMode: Text.NoWrap text: model.display } Keys.onPressed: { if (event.key == Qt.Key_Menu && hasActionList) { event.accepted = true; openActionMenu(item); } else if ((event.key == Qt.Key_Enter || event.key == Qt.Key_Return)) { event.accepted = true; GridView.view.model.trigger(index, "", null); if ("toggle" in root) { root.toggle(); } else { root.visible = false; } } } } diff --git a/package/contents/ui/ItemGridView.qml b/package/contents/ui/ItemGridView.qml index d367af7..7a73c7a 100644 --- a/package/contents/ui/ItemGridView.qml +++ b/package/contents/ui/ItemGridView.qml @@ -1,418 +1,423 @@ /*************************************************************************** * Copyright (C) 2015 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 org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 import org.kde.draganddrop 2.0 FocusScope { id: itemGrid signal keyNavLeft signal keyNavRight signal keyNavUp signal keyNavDown property bool dragEnabled: false property bool showLabels: true property alias usesPlasmaTheme: gridView.usesPlasmaTheme property int iconSize: root.iconSize property alias currentIndex: gridView.currentIndex property alias currentItem: gridView.currentItem property alias contentItem: gridView.contentItem property alias count: gridView.count property alias flow: gridView.flow property alias snapMode: gridView.snapMode property alias model: gridView.model property alias cellWidth: gridView.cellWidth property alias cellHeight: gridView.cellHeight property alias horizontalScrollBarPolicy: scrollArea.horizontalScrollBarPolicy property alias verticalScrollBarPolicy: scrollArea.verticalScrollBarPolicy onFocusChanged: { if (!focus) { currentIndex = -1; } } function currentRow() { if (currentIndex == -1) { return -1; } return Math.floor(currentIndex / Math.floor(width / cellWidth)); } function currentCol() { if (currentIndex == -1) { return -1; } return currentIndex - (currentRow() * Math.floor(width / cellWidth)); } function lastRow() { var columns = Math.floor(width / cellWidth); return Math.ceil(count / columns) - 1; } function tryActivate(row, col) { if (count) { var columns = Math.floor(width / cellWidth); var rows = Math.ceil(count / columns); row = Math.min(row, rows - 1); col = Math.min(col, columns - 1); currentIndex = Math.min(row ? ((Math.max(1, row) * columns) + col) : col, count - 1); gridView.forceActiveFocus(); } } function forceLayout() { gridView.forceLayout(); } ActionMenu { id: actionMenu onActionClicked: { visualParent.actionTriggered(actionId, actionArgument); } } DropArea { id: dropArea anchors.fill: parent onDragMove: { if (!dragEnabled || gridView.animating) { return; } var cPos = mapToItem(gridView.contentItem, event.x, event.y); var item = gridView.itemAt(cPos.x, cPos.y); if (item && item != kicker.dragSource && kicker.dragSource && kicker.dragSource.parent == gridView.contentItem) { item.GridView.view.model.moveRow(dragSource.itemIndex, item.itemIndex); } } Timer { id: resetAnimationDurationTimer interval: 80 repeat: false onTriggered: { gridView.animationDuration = interval - 20; } } PlasmaExtras.ScrollArea { id: scrollArea anchors.fill: parent focus: true horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff GridView { id: gridView property bool usesPlasmaTheme: false property bool animating: false property int animationDuration: dragEnabled ? resetAnimationDurationTimer.interval : 0 focus: true currentIndex: -1 move: Transition { enabled: itemGrid.dragEnabled SequentialAnimation { PropertyAction { target: gridView; property: "animating"; value: true } NumberAnimation { duration: gridView.animationDuration properties: "x, y" easing.type: Easing.OutQuad } PropertyAction { target: gridView; property: "animating"; value: false } } } moveDisplaced: Transition { enabled: itemGrid.dragEnabled SequentialAnimation { PropertyAction { target: gridView; property: "animating"; value: true } NumberAnimation { duration: gridView.animationDuration properties: "x, y" easing.type: Easing.OutQuad } PropertyAction { target: gridView; property: "animating"; value: false } } } keyNavigationWraps: false boundsBehavior: Flickable.StopAtBounds delegate: ItemGridDelegate { showLabel: showLabels } highlight: PlasmaComponents.Highlight {} highlightFollowsCurrentItem: true highlightMoveDuration: 0 onCountChanged: { animationDuration = 0; resetAnimationDurationTimer.start(); } onModelChanged: { currentIndex = -1; } Keys.onLeftPressed: { if (currentIndex == -1) { currentIndex = 0; return; } if (currentCol() != 0) { event.accepted = true; moveCurrentIndexLeft(); } else { itemGrid.keyNavLeft(); } } Keys.onRightPressed: { if (currentIndex == -1) { currentIndex = 0; return; } var columns = Math.floor(width / cellWidth); if (currentCol() != columns - 1 && currentIndex != count - 1) { event.accepted = true; moveCurrentIndexRight(); } else { itemGrid.keyNavRight(); } } Keys.onUpPressed: { if (currentIndex == -1) { currentIndex = 0; return; } if (currentRow() != 0) { event.accepted = true; moveCurrentIndexUp(); positionViewAtIndex(currentIndex, GridView.Contain); } else { itemGrid.keyNavUp(); } } Keys.onDownPressed: { if (currentIndex == -1) { currentIndex = 0; return; } if (currentRow() < itemGrid.lastRow()) { // Fix moveCurrentIndexDown()'s lack of proper spatial nav down // into partial columns. event.accepted = true; var columns = Math.floor(width / cellWidth); var newIndex = currentIndex + columns; currentIndex = Math.min(newIndex, count - 1); positionViewAtIndex(currentIndex, GridView.Contain); } else { itemGrid.keyNavDown(); } } } } MouseArea { anchors.fill: parent property int pressX: -1 property int pressY: -1 property int lastX: -1 property int lastY: -1 property Item pressedItem: null acceptedButtons: Qt.LeftButton | Qt.RightButton hoverEnabled: true function updatePositionProperties(x, y) { // Prevent hover event synthesis in QQuickWindow interfering // with keyboard navigation by ignoring repeated events with // identical coordinates. As the work done here would be re- // dundant in any case, these are safe to ignore. if (lastX == x && lastY == y) { return; } lastX = x; lastY = y; var cPos = mapToItem(gridView.contentItem, x, y); var item = gridView.itemAt(cPos.x, cPos.y); if (!item) { gridView.currentIndex = -1; pressedItem = null; } else { gridView.currentIndex = item.itemIndex; itemGrid.focus = (currentIndex != -1) } return item; } onPressed: { mouse.accepted = true; updatePositionProperties(mouse.x, mouse.y); pressX = mouse.x; pressY = mouse.y; if (mouse.button == Qt.RightButton) { if (gridView.currentItem) { if (gridView.currentItem.hasActionList) { var mapped = mapToItem(gridView.currentItem, mouse.x, mouse.y); gridView.currentItem.openActionMenu(mapped.x, mapped.y); } } else { var mapped = mapToItem(rootItem, mouse.x, mouse.y); contextMenu.open(mapped.x, mapped.y); } } else { pressedItem = gridView.currentItem; } } onReleased: { mouse.accepted = true; if (gridView.currentItem && gridView.currentItem == pressedItem) { if ("trigger" in gridView.model) { gridView.model.trigger(pressedItem.itemIndex, "", null); if ("toggle" in root) { root.toggle(); } else { root.visible = false; } } } else if (!pressedItem && mouse.button == Qt.LeftButton) { if ("toggle" in root) { root.toggle(); } else { root.visible = false; } } pressX = -1; pressY = -1; pressedItem = null; } onPressAndHold: { if (!dragEnabled) { pressX = -1; pressY = -1; return; } var cPos = mapToItem(gridView.contentItem, mouse.x, mouse.y); var item = gridView.itemAt(cPos.x, cPos.y); if (!item) { return; } if (!dragHelper.isDrag(pressX, pressY, mouse.x, mouse.y)) { kicker.dragSource = item; dragHelper.startDrag(kicker, item.url); } pressX = -1; pressY = -1; pressedItem = null; } onPositionChanged: { var item = updatePositionProperties(mouse.x, mouse.y); if (gridView.currentIndex != -1) { if (dragEnabled && pressX != -1 && dragHelper.isDrag(pressX, pressY, mouse.x, mouse.y)) { if ("pluginName" in item.m) { dragHelper.startDrag(kicker, item.url, item.icon, "text/x-plasmoidservicename", item.m.pluginName); } else { dragHelper.startDrag(kicker, item.url, item.icon); } kicker.dragSource = item; pressX = -1; pressY = -1; } } } onContainsMouseChanged: { if (!containsMouse) { - gridView.currentIndex = -1; + if (!actionMenu.opened) { + gridView.currentIndex = -1; + } + pressX = -1; pressY = -1; + lastX = -1; + lastY = -1; pressedItem = null; } } } } } diff --git a/package/metadata.desktop b/package/metadata.desktop index 83ffe2c..26332f1 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.10 +X-KDE-PluginInfo-Version=1.0.11 X-KDE-PluginInfo-Category=Application Launchers X-KDE-PluginInfo-Depends= X-KDE-PluginInfo-License=GPL v2+ X-KDE-PluginInfo-EnabledByDefault=true