Commit 9d9c4f16 authored by Noah Davis's avatar Noah Davis 🌵
Browse files

Kickoff: Refactor and tweak design

Changes:

- Now uses implicit content size to set the size of the plasmoid, which
fixes translated string elision issues.
- Much simpler code. This is a red patch.
- Reduced RAM usage. Pages that are not in use are unloaded. The models
aren't unloaded, so it shouldn't negatively affect performance in other
ways.
- Vertical movement and opacity animation when switching from normal
page to search view.
- Single horizontal movement and opacity animation when switching from
applications page to places page.
- Opacity animation when switching from favorites grid to apps list.
- KickoffListView and KickoffGridView now support PageUp, PageDown,
Home, End, Ctrl+Home and Ctrl+End shortcuts like lists and grids do on
the web. See https://www.w3.org/TR/wai-aria-practices-1.2/#grid
- KickoffListView/KickoffGridView keyboard navigation disables hovering
to change current item for 100ms. It's meant to filter out accidental
mouse movement while using the arrow keys to navigate.
- Add C...
parent 866d1438
......@@ -33,13 +33,17 @@
<label>How to display favorites: 0 = Grid, 1 = List</label>
<default>0</default>
</entry>
<entry name="gridAllowTwoLines" type="Bool">
<label>Whether to allow showing two lines in grid view.</label>
<default>true</default>
<entry name="applicationsDisplay" type="Int">
<label>How to display applications: 0 = Grid, 1 = List</label>
<default>1</default>
</entry>
<entry name="alphaSort" type="Bool">
<label>Whether to sort menu contents alphabetically or use manual/system sort order.</label>
<default>false</default>
</entry>
<entry name="pin" type="Bool">
<label>Whether the popup should remain open when another window is activated</label>
<default>false</default>
</entry>
</group>
</kcfg>
......@@ -2,122 +2,80 @@
SPDX-FileCopyrightText: 2013 Aurélien Gâteau <agateau@kde.org>
SPDX-FileCopyrightText: 2014-2015 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2021 Mikel Johnson <mikel5764@gmail.com>
SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.0
pragma Singleton
import org.kde.plasma.components 2.0 as PlasmaComponents // for Menu + MenuItem
import QtQuick 2.15
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PC2 // for Menu + MenuItem
import "code/tools.js" as Tools
Item {
id: actionMenuRoot
property QtObject menu
property Item visualParent
property variant actionList
signal actionClicked(string actionId, variant actionArgument)
onActionListChanged: refreshMenu();
function open(x, y) {
if (!actionList || !actionList.length) {
return;
}
if (x && y) {
menu.open(x, y);
} else {
menu.open();
}
id: root
property var actionList: menu.visualParent ? menu.visualParent.actionList : null
// Workaround for `plasmoid` context property not working in singletons
readonly property var plasmoid: KickoffSingleton.plasmoid
// Not a QQC1 Menu. It's actually a custom QObject that uses a QMenu.
readonly property PC2.Menu menu: PC2.Menu {
id: menu
visualParent: root.parent
placement: PlasmaCore.Types.BottomPosedLeftAlignedPopup
}
function refreshMenu() {
if (menu) {
menu.destroy();
}
visible: false
if (!actionList) {
return;
}
menu = contextMenuComponent.createObject(actionMenuRoot);
// actionList.forEach(function(actionItem) {
// var item = contextMenuItemComponent.createObject(menu, {
// "actionItem": actionItem,
// });
// });
fillMenu(menu, actionList);
Instantiator {
active: actionList != null
model: actionList
delegate: menuItemComponent
onObjectAdded: menu.addMenuItem(object)
onObjectRemoved: menu.removeMenuItem(object)
}
function fillMenu(menu, items) {
items.forEach(function(actionItem) {
if (actionItem.subActions) {
// This is a menu
var submenuItem = contextSubmenuItemComponent.createObject(
menu, { "actionItem" : actionItem });
fillMenu(submenuItem.submenu, actionItem.subActions);
Component { id: menuComponent; PC2.Menu {} }
Component {
id: menuItemComponent
PC2.MenuItem {
id: menuItem
required property var modelData
property PC2.Menu subMenu: if (modelData.subActions) {
return menuComponent.createObject(menuItem, {visualParent: menuItem.action})
} else {
var item = contextMenuItemComponent.createObject(
menu,
{
"actionItem": actionItem,
}
);
return null
}
});
}
Component {
id: contextMenuComponent
PlasmaComponents.Menu {
visualParent: actionMenuRoot.visualParent
}
}
Component {
id: contextSubmenuItemComponent
PlasmaComponents.MenuItem {
id: submenuItem
property variant actionItem
text: actionItem.text ? actionItem.text : ""
icon: actionItem.icon ? actionItem.icon : null
property variant submenu : submenu_
PlasmaComponents.Menu {
id: submenu_
visualParent: submenuItem.action
text: modelData.text ? modelData.text : ""
enabled: modelData.type !== "title" && ("enabled" in modelData ? modelData.enabled : true)
separator: modelData.type === "separator"
section: modelData.type === "title"
icon: modelData.icon ? modelData.icon : null
checkable: modelData.hasOwnProperty("checkable") ? modelData.checkable : false
checked: modelData.hasOwnProperty("checked") ? modelData.checked : false
Instantiator {
active: menuItem.subMenu != null
model: modelData.subActions
delegate: menuItemComponent
onObjectAdded: subMenu.addMenuItem(object)
onObjectRemoved: subMenu.removeMenuItem(object)
}
}
}
Component {
id: contextMenuItemComponent
PlasmaComponents.MenuItem {
property variant actionItem
text : actionItem.text ? actionItem.text : ""
enabled : actionItem.type !== "title" && ("enabled" in actionItem ? actionItem.enabled : true)
separator : actionItem.type === "separator"
section : actionItem.type === "title"
icon : actionItem.icon ? actionItem.icon : null
checkable : actionItem.checkable ? actionItem.checkable : false
checked : actionItem.checked ? actionItem.checked : false
onClicked: {
actionMenuRoot.actionClicked(actionItem.actionId, actionItem.actionArgument);
const modelActionTriggered = Tools.triggerAction(
menu.visualParent.view.model,
menu.visualParent.index,
modelData.actionId,
modelData.actionArgument
)
if (modelActionTriggered) {
root.plasmoid.expanded = false
}
}
}
}
......
/*
SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
SPDX-FileCopyrightText: 2012 Gregor Taetzner <gregor@freenet.de>
SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2015-2018 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2021 Mikel Johnson <mikel5764@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.private.kicker 0.1 as Kicker
FocusScope {
id: appViewContainer
property QtObject activatedSection: null
property string newBreadcrumbName: ""
signal appModelChange()
objectName: "ApplicationsGroupView"
property ListView listView: applicationsView.listView
function keyNavUp() {
return applicationsView.keyNavUp();
}
function keyNavDown() {
return applicationsView.keyNavDown();
}
function activateCurrentIndex() {
applicationsView.activatedItem = applicationsView.currentItem
applicationsView.moveRight()
}
function openContextMenu() {
applicationsView.currentItem.openActionMenu();
}
function reset() {
applicationsView.model = rootModel;
}
function refreshed() {
reset();
updatedLabelTimer.running = true;
}
Connections {
target: plasmoid
function onExpandedChanged() {
if (!plasmoid.expanded) {
reset();
}
}
}
Kicker.TriangleMouseFilter {
anchors.fill: parent
edge: LayoutMirroring.enabled ? Qt.LeftEdge : Qt.RightEdge
KickoffListView {
id: applicationsView
isManagerMode: true
anchors.fill: parent
property Item activatedItem: null
property var newModel: null
Behavior on opacity {
NumberAnimation {
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
}
focus: true
appView: true
model: rootModel
function moveRight() {
var childModel = activatedItem.activate()
if (childModel != null) {
appViewContainer.activatedSection = childModel.model
appViewContainer.newBreadcrumbName = childModel.name
appViewContainer.appModelChange()
}
}
onReset: appViewContainer.reset()
}
}
// Displays text when application list gets updated
Timer {
id: updatedLabelTimer
// We want to have enough time to show that applications have been updated even for those who disabled animations
interval: 1500
running: false
repeat: true
onRunningChanged: {
if (running) {
updatedLabel.opacity = 1;
applicationsView.listView.opacity = 0;
}
}
onTriggered: {
updatedLabel.opacity = 0;
applicationsView.listView.opacity = 1;
running = false;
}
}
PlasmaExtras.PlaceholderMessage {
id: updatedLabel
width: parent.width - (PlasmaCore.Units.largeSpacing * 4)
text: i18n("Updating applications…")
iconName: "view-refresh"
opacity: 0
visible: opacity != 0
anchors.centerIn: parent
Behavior on opacity {
NumberAnimation {
duration: PlasmaCore.Units.shortDuration
easing.type: Easing.InOutQuad
}
}
}
Component.onCompleted: {
rootModel.cleared.connect(refreshed);
}
} // appViewContainer
/*
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Templates 2.15 as T
import QtQml 2.15
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.kicker 0.1 as Kicker
BasePage {
id: root
sideBarComponent: KickoffListView {
id: sideBar
focus: true // needed for Loaders
model: KickoffSingleton.rootModel
delegate: KickoffItemDelegate {
id: itemDelegate
extendHoverMargins: true
width: view.availableWidth
isCategory: model.hasChildren
}
}
contentAreaComponent: VerticalStackView {
id: stackView
readonly property string preferredFavoritesViewObjectName: plasmoid.configuration.favoritesDisplay == 0 ? "favoritesGridView" : "favoritesListView"
readonly property Component preferredFavoritesViewComponent: plasmoid.configuration.favoritesDisplay == 0 ? favoritesGridViewComponent : favoritesListViewComponent
readonly property string preferredAppsViewObjectName: plasmoid.configuration.applicationsDisplay == 0 ? "applicationsGridView" : "applicationsListView"
readonly property Component preferredAppsViewComponent: plasmoid.configuration.applicationsDisplay == 0 ? applicationsGridViewComponent : applicationsListViewComponent
// NOTE: The 0 index modelForRow isn't supposed to be used. That's just how it works.
property int appsModelRow: 1
readonly property Kicker.AppsModel appsModel: KickoffSingleton.rootModel.modelForRow(appsModelRow)
focus: true
initialItem: preferredFavoritesViewComponent
Component {
id: favoritesListViewComponent
KickoffListView {
id: favoritesListView
objectName: "favoritesListView"
mainContentView: true
focus: true
model: KickoffSingleton.rootModel.favoritesModel
}
}
Component {
id: favoritesGridViewComponent
KickoffGridView {
id: favoritesGridView
objectName: "favoritesGridView"
focus: true
model: KickoffSingleton.rootModel.favoritesModel
}
}
Component {
id: applicationsListViewComponent
KickoffListView {
id: applicationsListView
objectName: "applicationsListView"
mainContentView: true
model: stackView.appsModel
section.property: model && model.description == "KICKER_ALL_MODEL" ? "display" : ""
section.criteria: ViewSection.FirstCharacter
}
}
Component {
id: applicationsGridViewComponent
KickoffGridView {
id: applicationsGridView
objectName: "applicationsGridView"
model: stackView.appsModel
}
}
onPreferredFavoritesViewComponentChanged: {
if (root.sideBarItem != null && root.sideBarItem.currentIndex === 0) {
stackView.replace(stackView.preferredFavoritesViewComponent)
}
}
onPreferredAppsViewComponentChanged: {
if (root.sideBarItem != null && root.sideBarItem.currentIndex > 1) {
stackView.replace(stackView.preferredAppsViewComponent)
}
}
Connections {
target: root.sideBarItem
function onCurrentIndexChanged() {
// Only update row index if the condition is met.
// The 0 index modelForRow isn't supposed to be used. That's just how it works.
if (root.sideBarItem.currentIndex > 0 && sideBarItem.currentItem.model.hasChildren) {
appsModelRow = root.sideBarItem.currentIndex
}
if (root.sideBarItem.currentIndex === 0
&& stackView.currentItem.objectName !== stackView.preferredFavoritesViewObjectName) {
stackView.replace(stackView.preferredFavoritesViewComponent)
} else if (root.sideBarItem.currentIndex === 1
&& stackView.currentItem.objectName !== "applicationsListView") {
// Always use list view for alphabetical apps view since grid view doesn't have sections
// TODO: maybe find a way to have a list view with grids in each section?
stackView.replace(applicationsListViewComponent)
} else if (root.sideBarItem.currentIndex > 1
&& stackView.currentItem.objectName !== stackView.preferredAppsViewObjectName) {
stackView.replace(stackView.preferredAppsViewComponent)
}
}
}
Connections {
target: plasmoid
function onExpandedChanged() {
if(!plasmoid.expanded) {
KickoffSingleton.contentArea.currentItem.forceActiveFocus()
}
}
}
}
// NormalPage doesn't get destroyed when deactivated, so the binding uses
// StackView.status and visible. This way the bindings are reset when
// NormalPage is Activated again.
Binding {
target: KickoffSingleton
property: "sideBar"
value: root.sideBarItem
when: root.T.StackView.status === T.StackView.Active && root.visible
restoreMode: Binding.RestoreBinding
}
Binding {
target: KickoffSingleton
property: "contentArea"
value: root.contentAreaItem.currentItem // NOT just root.contentAreaItem
when: root.T.StackView.status === T.StackView.Active && root.visible
restoreMode: Binding.RestoreBinding
}
}
/*
SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
SPDX-FileCopyrightText: 2012 Gregor Taetzner <gregor@freenet.de>
SPDX-FileCopyrightText: 2014 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2015-2018 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2021 Mikel Johnson <mikel5764@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
FocusScope {
id: appViewContainer
property QtObject activatedSection: null
property string rootBreadcrumbName: ""
signal appModelChange()
onAppModelChange: {
if (activatedSection != null) {
applicationsView.clearBreadcrumbs();
applicationsView.listView.model = activatedSection;
}
}
objectName: "ApplicationsView"
property ListView listView: applicationsView.listView
function keyNavUp() {
return applicationsView.keyNavUp();
}
function keyNavDown() {
return applicationsView.keyNavDown();
}
function activateCurrentIndex() {
applicationsView.state = "OutgoingLeft";
}
function openContextMenu() {
applicationsView.currentItem.openActionMenu();
}
function deactivateCurrentIndex() {
if (crumbModel.count > 0) { // this is not the case when switching from the right sidebar to the left when going "left"
breadcrumbsElement.children[crumbModel.count-1].clickCrumb();
applicationsView.state = "OutgoingRight";
return true;
}
return false;
}
function reset() {
applicationsView.model = activatedSection;
applicationsView.clearBreadcrumbs();
if (applicationsView.model == null) {
applicationsView.currentIndex = -1
} else {
applicationsView.currentIndex = 0
}
}
function refreshed() {
reset();
updatedLabelTimer.running = true;
}
Connections {
target: plasmoid
function onExpandedChanged() {
if (!plasmoid.expanded) {
reset();
}
}
}
Connections {
target: rootBreadcrumb
function onRootClick() {
applicationsView.newModel = activatedSection;
}
}
Item {
id: crumbContainer
anchors {
top: parent.top
left: parent.left
right: parent.right
}
visible: applicationsView.model != null && applicationsView.model.description && applicationsView.model.description != "KICKER_ALL_MODEL"
height: visible ? breadcrumbFlickable.height : 0
Behavior on opacity {
NumberAnimation {
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
}
PlasmaCore.SvgItem {
id: horizontalSeparator
opacity: applicationsView.listView.contentY !== 0