/* Copyright (C) 2011 Martin Gräßlin Copyright (C) 2012 Gregor Taetzner Copyright (C) 2012 Marco Martin Copyright (C) 2013 2014 David Edmundson Copyright 2014 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 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.3 import org.kde.plasma.plasmoid 2.0 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid as PlasmaCore import org.kde.ksvg as KSvg import org.kde.plasma.plasma5support as Plasma5Support import org.kde.kirigami 2.20 as Kirigami import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.private.kicker 0.1 as Kicker Item { id: root Layout.minimumWidth: Kirigami.Units.gridUnit * 26 Layout.maximumWidth: Layout.minimumWidth Layout.minimumHeight: Kirigami.Units.gridUnit * 34 Layout.maximumHeight: Layout.minimumHeight property string previousState property bool switchTabsOnHover: plasmoid.configuration.switchTabsOnHover property Item currentView: mainTabGroup.currentTab.decrementCurrentIndex ? mainTabGroup.currentTab : mainTabGroup.currentTab.item property KickoffButton firstButton: null property var configMenuItems property QtObject globalFavorites: rootModelFavorites state: "Normal" onFocusChanged: { header.input.forceActiveFocus(); } function switchToInitial() { if (firstButton != null) { root.state = "Normal"; mainTabGroup.currentTab = firstButton.tab; tabBar.currentTab = firstButton; header.query = "" } } Kicker.DragHelper { id: dragHelper dragIconSize: Kirigami.Units.iconSizes.medium onDropped: kickoff.dragSource = null } Kicker.AppsModel { id: rootModel autoPopulate: false appletInterface: plasmoid appNameFormat: plasmoid.configuration.showAppsByName ? 0 : 1 flat: false sorted: plasmoid.configuration.alphaSort showSeparators: false showTopLevelItems: true favoritesModel: Kicker.KAStatsFavoritesModel { id: rootModelFavorites favorites: plasmoid.configuration.favorites onFavoritesChanged: { plasmoid.configuration.favorites = favorites; } } Component.onCompleted: { favoritesModel.initForClient("org.kde.plasma.kickoff.favorites.instance-" + plasmoid.id) if (!plasmoid.configuration.favoritesPortedToKAstats) { favoritesModel.portOldFavorites(plasmoid.configuration.favorites); plasmoid.configuration.favoritesPortedToKAstats = true; } rootModel.refresh(); } } Plasma5Support.DataSource { id: pmSource engine: "powermanagement" connectedSources: ["PowerDevil"] } KSvg.Svg { id: arrowsSvg imagePath: "widgets/arrows" size: "16x16" } Header { id: header } Item { id: mainArea anchors.topMargin: mainTabGroup.state == "top" ? Kirigami.Units.smallSpacing : 0 Kirigami.Page { id: mainPage // Set anchors anchors.fill: parent // Create a row for the tab buttons Kirigami.PageRow { id: tabRow // Set row orientation based on plasmoid location orientation: { switch (plasmoid.location) { case PlasmaCore.Types.LeftEdge: case PlasmaCore.Types.RightEdge: return Qt.Horizontal default: return Qt.Vertical } } // Add tab buttons PlasmaComponents.Button { text: i18n("Favorites") onClicked: mainPage.currentIndex = 0 } PlasmaComponents.Button { text: i18n("Applications") onClicked: mainPage.currentIndex = 1 } PlasmaComponents.Button { text: i18n("Computer") onClicked: mainPage.currentIndex = 2 } PlasmaComponents.Button { text: i18n("Recently Used") onClicked: mainPage.currentIndex = 3 } PlasmaComponents.Button { text: i18n("Often Used") onClicked: mainPage.currentIndex = 4 } PlasmaComponents.Button { text: i18n("Leave") onClicked: mainPage.currentIndex = 5 } PlasmaComponents.Button { text: i18n("Search") onClicked: root.state = "Search" } } // Add pages Kirigami.Page { id: favoritesPage anchors.fill: parent visible: mainPage.currentIndex === 0 // Add contents of the Favorites view here } Kirigami.Page { id: applicationsPage anchors.fill: parent visible: mainPage.currentIndex === 1 // Add contents of the Applications view here } Kirigami.Page { id: computerPage anchors.fill: parent visible: mainPage.currentIndex === 2 // Add contents of the Computer view here } Kirigami.Page { id: recentlyUsedPage anchors.fill: parent visible: mainPage.currentIndex === 3 // Add contents of the Recently Used view here } Kirigami.Page { id: oftenUsedPage anchors.fill: parent visible: mainPage.currentIndex === 4 // Add contents of the Often Used view here } Kirigami.Page { id: leavePage anchors.fill: parent visible: mainPage.currentIndex === 5 // Add contents of the Leave view here } Kirigami.Page { id: searchPage anchors.fill: parent visible: root.state === "Search" // Add contents of the Search view here } // Handle the plasmoid location states states: [ State { name: "left" PropertyChanges { target: tabRow rotation: 0 } PropertyChanges { target: tabRow anchors { left: parent.left top: parent.top bottom: parent.bottom } } }, State { name: "top" PropertyChanges { target: tabRow rotation: -90 } PropertyChanges { target: tabRow anchors { top: parent.top left: parent.left right: parent.right } } }, State { name: "right" PropertyChanges { target: tabRow rotation: 180 } PropertyChanges { target: tabRow anchors { right: parent.right top: parent.top bottom: parent.bottom } } }, State { name: "bottom" PropertyChanges { target: tabRow rotation: 90 } PropertyChanges { target: tabRow anchors { bottom: parent.bottom left: parent.left right: parent.right } } } ] // Set initial plasmoid state state: { switch (plasmoid.location) { case PlasmaCore.Types.LeftEdge: return "left" case PlasmaCore.Types.TopEdge: return "top" case PlasmaCore.Types.RightEdge: return "right" case PlasmaCore.Types.BottomEdge: default: return "bottom" } } } } PlasmaComponents.TabBar { id: tabBar count: 5 // updated in createButtons() Behavior on width { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InQuad; } enabled: plasmoid.expanded } Behavior on height { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InQuad; } enabled: plasmoid.expanded } tabPosition: { switch (plasmoid.location) { case PlasmaCore.Types.TopEdge: return Qt.TopEdge; case PlasmaCore.Types.LeftEdge: return Qt.LeftEdge; case PlasmaCore.Types.RightEdge: return Qt.RightEdge; default: return Qt.BottomEdge; } } onCurrentTabChanged: header.input.forceActiveFocus(); Connections { target: plasmoid function onExpandedChanged() { if(menuItemsChanged()) { createButtons(); } if (!plasmoid.expanded) { switchToInitial(); } } } } // tabBar KSvg.SvgItem { id: tabBarSeparator svg: KSvg.Svg { id: tabBarSeparatorLine imagePath: "widgets/line" } } MouseArea { anchors.fill: tabBar property var oldPos: null enabled: root.state !== "Search" hoverEnabled: root.switchTabsOnHover onExited: { // Reset so we switch immediately when MouseArea is entered // freshly, e.g. from the panel. oldPos = null; clickTimer.stop(); } onPositionChanged: { // Reject multiple events with the same coordinates that QQuickWindow // synthesizes. if (oldPos === Qt.point(mouse.x, mouse.y)) { return; } var button = tabBar.layout.childAt(mouse.x, mouse.y); if (!button || button.objectName !== "KickoffButton") { clickTimer.stop(); return; } // Switch immediately when MouseArea was freshly entered, e.g. // from the panel. if (oldPos === null) { oldPos = Qt.point(mouse.x, mouse.y); clickTimer.stop(); button.clicked(); return; } var dx = (mouse.x - oldPos.x); var dy = (mouse.y - oldPos.y); // Check Manhattan length against drag distance to get a decent // pointer motion vector. if ((Math.abs(dx) + Math.abs(dy)) > Qt.styleHints.startDragDistance) { if (tabBar.currentTab !== button) { var tabBarPos = mapToItem(tabBar, oldPos.x, oldPos.y); oldPos = Qt.point(mouse.x, mouse.y); var angleMouseMove = Math.atan2(dy, dx) * 180 / Math.PI; var angleToCornerA = 0; var angleToCornerB = 0; switch (plasmoid.location) { case PlasmaCore.Types.TopEdge: { angleToCornerA = Math.atan2(tabBar.height - tabBarPos.y, 0 - tabBarPos.x); angleToCornerB = Math.atan2(tabBar.height - tabBarPos.y, tabBar.width - tabBarPos.x); break; } case PlasmaCore.Types.LeftEdge: { angleToCornerA = Math.atan2(0 - tabBarPos.y, tabBar.width - tabBarPos.x); angleToCornerB = Math.atan2(tabBar.height - tabBarPos.y, tabBar.width - tabBarPos.x); break; } case PlasmaCore.Types.RightEdge: { angleToCornerA = Math.atan2(0 - tabBarPos.y, 0 - tabBarPos.x); angleToCornerB = Math.atan2(tabBar.height - tabBarPos.y, 0 - tabBarPos.x); break; } // PlasmaCore.Types.BottomEdge default: { angleToCornerA = Math.atan2(0 - tabBarPos.y, 0 - tabBarPos.x); angleToCornerB = Math.atan2(0 - tabBarPos.y, tabBar.width - tabBarPos.x); } } // Degrees are nicer to debug than radians. angleToCornerA = angleToCornerA * 180 / Math.PI; angleToCornerB = angleToCornerB * 180 / Math.PI; var lower = Math.min(angleToCornerA, angleToCornerB); var upper = Math.max(angleToCornerA, angleToCornerB); // If the motion vector is outside the angle range from oldPos to the // relevant tab bar corners, switch immediately. Otherwise start the // timer, which gets aborted should the pointer exit the tab bar // early. var inRange = (lower < angleMouseMove == angleMouseMove < upper); // Mirror-flip. if (plasmoid.location === PlasmaCore.Types.RightEdge ? inRange : !inRange) { clickTimer.stop(); button.clicked(); return; } else { clickTimer.pendingButton = button; clickTimer.start(); } } else { oldPos = Qt.point(mouse.x, mouse.y); } } } onClicked: { clickTimer.stop(); var button = tabBar.layout.childAt(mouse.x, mouse.y); if (!button || button.objectName !== "KickoffButton") { return; } button.clicked(); } Timer { id: clickTimer property Item pendingButton: null interval: 250 onTriggered: { if (pendingButton) { pendingButton.clicked(); } } } } Keys.forwardTo: [tabBar.layout] Keys.onPressed: { if (mainTabGroup.currentTab == applicationsPage) { if (event.key !== Qt.Key_Tab) { root.state = "Applications"; } } switch(event.key) { case Qt.Key_Up: { currentView.decrementCurrentIndex(); event.accepted = true; break; } case Qt.Key_Down: { currentView.incrementCurrentIndex(); event.accepted = true; break; } case Qt.Key_Left: { if (header.input.focus && header.state == "query") { break; } if (!currentView.deactivateCurrentIndex()) { if (root.state == "Applications") { mainTabGroup.currentTab = firstButton.tab; tabBar.currentTab = firstButton; } root.state = "Normal" } event.accepted = true; break; } case Qt.Key_Right: { if (header.input.focus && header.state == "query") { break; } currentView.activateCurrentIndex(); event.accepted = true; break; } case Qt.Key_Tab: { root.state == "Applications" ? root.state = "Normal" : root.state = "Applications"; event.accepted = true; break; } case Qt.Key_Enter: case Qt.Key_Return: { currentView.activateCurrentIndex(1); event.accepted = true; break; } case Qt.Key_Escape: { if (header.state != "query") { plasmoid.expanded = false; } else { header.query = ""; } event.accepted = true; break; } case Qt.Key_Menu: { currentView.openContextMenu(); event.accepted = true; break; } default: if (!header.input.focus) { header.input.forceActiveFocus(); } } } states: [ State { name: "Normal" PropertyChanges { target: root Keys.forwardTo: [tabBar.layout] } PropertyChanges { target: tabBar //Set the opacity and NOT the visibility, as visibility is recursive //and this binding would be executed also on popup show/hide //as recommended by the docs: https://doc.qt.io/qt-5/qml-qtquick-item.html#visible-prop //plus, it triggers https://bugreports.qt.io/browse/QTBUG-66907 //in which a mousearea may think it's under the mouse while it isn't opacity: tabBar.count > 1 ? 1 : 0 } }, State { name: "Applications" PropertyChanges { target: root Keys.forwardTo: [root] } PropertyChanges { target: tabBar opacity: tabBar.count > 1 ? 1 : 0 } }, State { name: "Search" PropertyChanges { target: tabBar opacity: 0 } PropertyChanges { target: mainTabGroup currentTab: searchPage } PropertyChanges { target: root Keys.forwardTo: [root] } } ] // states function getButtonDefinition(name) { switch(name) { case "bookmark": return {id: "bookmarkButton", tab: favoritesPage, iconSource: "bookmarks", text: i18n("Favorites")}; case "application": return {id: "applicationButton", tab: applicationsPage, iconSource: "applications-other", text: i18n("Applications")}; case "computer": return {id: "computerButton", tab: systemPage, iconSource: pmSource.data["PowerDevil"] && pmSource.data["PowerDevil"]["Is Lid Present"] ? "computer-laptop" : "computer", text: i18n("Computer")}; case "used": return {id: "usedButton", tab: recentlyUsedPage, iconSource: "view-history", text: i18n("History")}; case "oftenUsed": return {id: "usedButton", tab: oftenUsedPage, iconSource: "office-chart-pie", text: i18n("Often Used")}; case "leave": return {id: "leaveButton", tab: leavePage, iconSource: "system-log-out", text: i18n("Leave")}; } } Component { id: kickoffButton KickoffButton {} } Component.onCompleted: { createButtons(); } function getEnabled(configuration) { var res = []; for(var i = 0; i < configuration.length; i++) { var confItemName = configuration[i].substring(0, configuration[i].indexOf(":")); var confItemEnabled = configuration[i].substring(configuration[i].length-1) === "t"; if(confItemEnabled) { res.push(confItemName); } } return res; } function createButtons() { configMenuItems = plasmoid.configuration.menuItems; var menuItems = getEnabled(plasmoid.configuration.menuItems); tabBar.count = menuItems.length // remove old menu items for(var i = tabBar.layout.children.length -1; i >= 0; i--) { if(tabBar.layout.children[i].objectName === "KickoffButton") { tabBar.layout.children[i].destroy(); } } for (var i = 0; i < menuItems.length; i++) { var props = getButtonDefinition(menuItems[i]); var button = kickoffButton.createObject(tabBar.layout, props); if(i === 0) { firstButton = button; switchToInitial(); } } } function menuItemsChanged() { if(configMenuItems.length !== plasmoid.configuration.menuItems.length) { return true; } for(var i = 0; i < configMenuItems.length; i++) { if(configMenuItems[i] !== plasmoid.configuration.menuItems[i]) { return true; } } return false; } }