import QtQuick import Nebula.UI import Nebula.Applications Item { id: root property real yOffset: 0 implicitWidth: 420 implicitHeight: 14 + searchField.implicitHeight + 14 + bodyColumn.implicitHeight + 14 + footer.height + 14 transform: Translate { y: root.yOffset } transformOrigin: Item.TopLeft Accessible.role: Accessible.Dialog Accessible.name: qsTr("Launcher") readonly property bool usingInstalledApps: ApplicationService.count > 0 readonly property bool searching: searchField.text.trim().length > 0 readonly property var mockMatches: { const query = searchField.text.trim().toLowerCase() const apps = MockApps.apps if (!query) return apps return apps.filter((app) => { const haystack = (app.name + " " + (app.comment || "")).toLowerCase() return haystack.indexOf(query) !== -1 }) } readonly property int resultCount: usingInstalledApps ? ApplicationService.filteredModel.count : mockMatches.length Keys.onEscapePressed: { event.accepted = true ShellState.closeLauncher() } Connections { target: ShellState function onLauncherOpenChanged() { if (ShellState.launcherOpen) { searchField.text = "" searchField.input.forceActiveFocus() } else { searchField.text = "" } } } NebulaCard { id: card anchors.fill: parent MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton } } Column { id: column anchors.fill: parent anchors.margins: 14 spacing: 0 NebulaTextField { id: searchField width: parent.width placeholderText: qsTr("Search applications") onTextChanged: ApplicationService.filter = text } Item { width: 1 height: 14 } Flickable { id: body width: parent.width height: Math.max(0, column.height - searchField.height - footer.height - 29) clip: true contentWidth: width contentHeight: bodyColumn.implicitHeight boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick Column { id: bodyColumn width: body.width spacing: 18 Column { width: parent.width spacing: 8 visible: !root.searching && !root.usingInstalledApps && MockApps.recents.length > 0 Text { text: qsTr("Recent") color: Theme.textSecondary font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.DemiBold font.letterSpacing: 0.4 } Grid { id: recentGrid width: parent.width columns: 4 columnSpacing: 6 rowSpacing: 6 Repeater { model: MockApps.recents delegate: AppItem { required property var modelData width: (recentGrid.width - 18) / 4 variant: "recent" item: modelData onSelected: root.launchApp(item) } } } } Column { width: parent.width spacing: 8 Text { text: root.searching ? qsTr("Results") : qsTr("Applications") color: Theme.textSecondary font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.DemiBold font.letterSpacing: 0.4 } Grid { id: appGrid visible: root.resultCount > 0 width: parent.width columns: 4 columnSpacing: 6 rowSpacing: 6 Repeater { model: root.usingInstalledApps ? ApplicationService.filteredModel : [] delegate: AppItem { required property string desktopId required property string name required property string comment required property string iconName width: (appGrid.width - 18) / 4 variant: "tile" item: ({ desktopId: desktopId, name: name, comment: comment, iconName: iconName }) onSelected: root.launchApp(item) } } Repeater { model: root.usingInstalledApps ? [] : root.mockMatches delegate: AppItem { required property var modelData width: (appGrid.width - 18) / 4 variant: "tile" item: modelData onSelected: root.launchApp(item) } } } Text { visible: root.resultCount === 0 width: parent.width topPadding: 8 text: qsTr("No matching applications") color: Theme.textTertiary font.family: Theme.fontFamily font.pixelSize: Theme.fontSize } } Column { width: parent.width spacing: 8 visible: !root.searching && !root.usingInstalledApps && MockApps.projects.length > 0 Text { text: qsTr("Recent projects") color: Theme.textSecondary font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.DemiBold font.letterSpacing: 0.4 } Column { width: parent.width spacing: 2 Repeater { model: MockApps.projects delegate: AppItem { required property var modelData width: parent.width variant: "row" item: modelData onSelected: root.openProject(item) } } } } } } Item { width: 1 height: 14 } Rectangle { width: parent.width height: 1 color: Theme.border } Item { id: footer width: parent.width height: 38 Row { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter spacing: 10 Rectangle { width: 28 height: 28 radius: 14 color: Theme.accentSoft anchors.verticalCenter: parent.verticalCenter Text { anchors.centerIn: parent text: MockApps.user.initials color: Theme.accentStrong font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeApp font.weight: Font.DemiBold } } Column { anchors.verticalCenter: parent.verticalCenter spacing: 0 Text { text: MockApps.user.name color: Theme.textPrimary font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.DemiBold } Text { text: qsTr("Local session") color: Theme.textTertiary font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } } } Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 2 NebulaIconButton { id: restartButton label: qsTr("Restart") onClicked: console.info("nebula: restart requested") NebulaIcon { name: "restart" size: 16 color: restartButton.iconColor } } NebulaIconButton { id: shutdownButton label: qsTr("Shut down") danger: true onClicked: console.info("nebula: shutdown requested") NebulaIcon { name: "power" size: 16 color: shutdownButton.iconColor } } } } } function launchApp(app) { if (app && app.desktopId) ApplicationService.launch(app.desktopId) ShellState.closeLauncher() } function openProject(project) { ShellState.closeLauncher() } }