Add focused window controls to top bar
Introduces a new `WindowControls` component in the desktop top bar with minimize, maximize/restore, and close actions for the focused window. `WindowService` now exposes focused-window presence/maximized state plus focused-window convenience actions to support this UI. Also extends Nebula UI with a reusable `NebulaToolTip`, keyboard/pressed-state support in `NebulaIconButton`, new window-control glyphs in `NebulaIcon`, and theme tokens for pressed/danger hover states, with CMake and README updates to register/document the new components.
This commit is contained in:
@@ -67,6 +67,11 @@ int WindowService::windowCount() const
|
||||
return m_model->count();
|
||||
}
|
||||
|
||||
bool WindowService::hasFocusedWindow() const
|
||||
{
|
||||
return !m_focusedWindowId.isEmpty();
|
||||
}
|
||||
|
||||
QString WindowService::focusedWindowId() const
|
||||
{
|
||||
return m_focusedWindowId;
|
||||
@@ -87,6 +92,11 @@ QString WindowService::focusedApplicationName() const
|
||||
return m_focusedApplicationName;
|
||||
}
|
||||
|
||||
bool WindowService::focusedWindowMaximized() const
|
||||
{
|
||||
return m_focusedWindowMaximized;
|
||||
}
|
||||
|
||||
bool WindowService::focusWindow(const QString &windowId)
|
||||
{
|
||||
return m_backend->focusWindow(parseWindowId(windowId));
|
||||
@@ -112,6 +122,38 @@ bool WindowService::minimizeWindow(const QString &windowId)
|
||||
return m_backend->minimizeWindow(parseWindowId(windowId));
|
||||
}
|
||||
|
||||
bool WindowService::minimizeFocusedWindow()
|
||||
{
|
||||
if (m_focusedWindowId.isEmpty())
|
||||
return false;
|
||||
return minimizeWindow(m_focusedWindowId);
|
||||
}
|
||||
|
||||
bool WindowService::toggleMaximizeFocusedWindow()
|
||||
{
|
||||
if (m_focusedWindowId.isEmpty())
|
||||
return false;
|
||||
|
||||
const bool restore = m_focusedWindowMaximized;
|
||||
const bool ok = restore ? restoreWindow(m_focusedWindowId)
|
||||
: maximizeWindow(m_focusedWindowId);
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
if (m_focusedWindowMaximized == restore) {
|
||||
m_focusedWindowMaximized = !restore;
|
||||
emit focusChanged();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowService::closeFocusedWindow()
|
||||
{
|
||||
if (m_focusedWindowId.isEmpty())
|
||||
return false;
|
||||
return closeWindow(m_focusedWindowId);
|
||||
}
|
||||
|
||||
bool WindowService::setWindowFloating(const QString &windowId, bool floating)
|
||||
{
|
||||
return m_backend->setWindowFloating(parseWindowId(windowId), floating);
|
||||
@@ -154,6 +196,7 @@ void WindowService::syncFromBackend()
|
||||
QString title;
|
||||
QString appId;
|
||||
QString appName;
|
||||
bool maximized = false;
|
||||
|
||||
if (const WindowInfo *focused = m_model->windowById(focusedId)) {
|
||||
if (!focused->minimized) {
|
||||
@@ -161,17 +204,20 @@ void WindowService::syncFromBackend()
|
||||
title = focused->title;
|
||||
appId = focused->appId.isEmpty() ? focused->windowClass : focused->appId;
|
||||
appName = focused->displayName;
|
||||
maximized = focused->maximized;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_focusedWindowId != windowId
|
||||
|| m_focusedWindowTitle != title
|
||||
|| m_focusedApplicationId != appId
|
||||
|| m_focusedApplicationName != appName) {
|
||||
|| m_focusedApplicationName != appName
|
||||
|| m_focusedWindowMaximized != maximized) {
|
||||
m_focusedWindowId = windowId;
|
||||
m_focusedWindowTitle = title;
|
||||
m_focusedApplicationId = appId;
|
||||
m_focusedApplicationName = appName;
|
||||
m_focusedWindowMaximized = maximized;
|
||||
emit focusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ class WindowService : public QObject
|
||||
Q_PROPERTY(WindowModel *model READ model CONSTANT)
|
||||
Q_PROPERTY(WorkspaceModel *workspaceModel READ workspaceModel CONSTANT)
|
||||
Q_PROPERTY(int windowCount READ windowCount NOTIFY windowCountChanged)
|
||||
Q_PROPERTY(bool hasFocusedWindow READ hasFocusedWindow NOTIFY focusChanged)
|
||||
Q_PROPERTY(QString focusedWindowId READ focusedWindowId NOTIFY focusChanged)
|
||||
Q_PROPERTY(QString focusedWindowTitle READ focusedWindowTitle NOTIFY focusChanged)
|
||||
Q_PROPERTY(QString focusedApplicationId READ focusedApplicationId NOTIFY focusChanged)
|
||||
Q_PROPERTY(QString focusedApplicationName READ focusedApplicationName NOTIFY focusChanged)
|
||||
Q_PROPERTY(bool focusedWindowMaximized READ focusedWindowMaximized NOTIFY focusChanged)
|
||||
|
||||
public:
|
||||
enum SnapEdge {
|
||||
@@ -42,16 +44,21 @@ public:
|
||||
WorkspaceModel *workspaceModel() const;
|
||||
int windowCount() const;
|
||||
|
||||
bool hasFocusedWindow() const;
|
||||
QString focusedWindowId() const;
|
||||
QString focusedWindowTitle() const;
|
||||
QString focusedApplicationId() const;
|
||||
QString focusedApplicationName() const;
|
||||
bool focusedWindowMaximized() const;
|
||||
|
||||
Q_INVOKABLE bool focusWindow(const QString &windowId);
|
||||
Q_INVOKABLE bool closeWindow(const QString &windowId);
|
||||
Q_INVOKABLE bool maximizeWindow(const QString &windowId);
|
||||
Q_INVOKABLE bool restoreWindow(const QString &windowId);
|
||||
Q_INVOKABLE bool minimizeWindow(const QString &windowId);
|
||||
Q_INVOKABLE bool minimizeFocusedWindow();
|
||||
Q_INVOKABLE bool toggleMaximizeFocusedWindow();
|
||||
Q_INVOKABLE bool closeFocusedWindow();
|
||||
Q_INVOKABLE bool setWindowFloating(const QString &windowId, bool floating);
|
||||
Q_INVOKABLE bool moveWindow(const QString &windowId, int x, int y);
|
||||
Q_INVOKABLE bool resizeWindow(const QString &windowId, int width, int height);
|
||||
@@ -79,4 +86,5 @@ private:
|
||||
QString m_focusedWindowTitle;
|
||||
QString m_focusedApplicationId;
|
||||
QString m_focusedApplicationName;
|
||||
bool m_focusedWindowMaximized = false;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ set(NEBULA_UI_QML_FILES
|
||||
NebulaCard.qml
|
||||
NebulaIconButton.qml
|
||||
NebulaTextField.qml
|
||||
NebulaToolTip.qml
|
||||
FocusFrame.qml
|
||||
NebulaIcon.qml
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import Nebula.UI
|
||||
This module contains reusable visual language only:
|
||||
|
||||
- colour, spacing, type, and motion tokens (`Theme`)
|
||||
- generic controls (`NebulaButton`, `NebulaCard`, `NebulaIconButton`, `NebulaTextField`)
|
||||
- generic controls (`NebulaButton`, `NebulaCard`, `NebulaIconButton`, `NebulaTextField`, `NebulaToolTip`)
|
||||
- focus treatment (`FocusFrame`)
|
||||
- compact vector icons (`NebulaIcon`)
|
||||
|
||||
|
||||
@@ -82,6 +82,18 @@ Item {
|
||||
case "application":
|
||||
paintApplication(ctx)
|
||||
break
|
||||
case "minimize":
|
||||
paintMinimize(ctx)
|
||||
break
|
||||
case "maximize":
|
||||
paintMaximize(ctx)
|
||||
break
|
||||
case "restore":
|
||||
paintRestore(ctx)
|
||||
break
|
||||
case "close":
|
||||
paintClose(ctx)
|
||||
break
|
||||
default:
|
||||
paintApplication(ctx)
|
||||
break
|
||||
@@ -192,6 +204,41 @@ Item {
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
function paintMinimize(ctx) {
|
||||
ctx.lineWidth = 1.7
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(6.4, 16.2)
|
||||
ctx.lineTo(17.6, 16.2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
function paintMaximize(ctx) {
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.beginPath()
|
||||
roundedRect(ctx, 6.2, 6.2, 11.6, 11.6, 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
function paintRestore(ctx) {
|
||||
ctx.lineWidth = 1.45
|
||||
ctx.beginPath()
|
||||
roundedRect(ctx, 8.6, 5.6, 9.4, 9.4, 1.7)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
roundedRect(ctx, 5.6, 8.6, 9.4, 9.4, 1.7)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
function paintClose(ctx) {
|
||||
ctx.lineWidth = 1.7
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(7.3, 7.3)
|
||||
ctx.lineTo(16.7, 16.7)
|
||||
ctx.moveTo(16.7, 7.3)
|
||||
ctx.lineTo(7.3, 16.7)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
function paintApplication(ctx) {
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.beginPath()
|
||||
|
||||
@@ -7,6 +7,7 @@ Item {
|
||||
property bool active: false
|
||||
property bool danger: false
|
||||
readonly property bool hovered: hover.containsMouse
|
||||
readonly property bool pressed: hover.pressed
|
||||
readonly property color iconColor: {
|
||||
if (danger && hovered)
|
||||
return Theme.danger
|
||||
@@ -23,16 +24,32 @@ Item {
|
||||
|
||||
implicitWidth: Theme.controlHeight
|
||||
implicitHeight: Theme.controlHeight
|
||||
activeFocusOnTab: true
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: root.label
|
||||
Accessible.onPressAction: root.clicked()
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
event.accepted = true
|
||||
root.clicked()
|
||||
}
|
||||
Keys.onSpacePressed: {
|
||||
event.accepted = true
|
||||
root.clicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.radiusSmall
|
||||
color: {
|
||||
if (root.danger && hover.pressed)
|
||||
return Theme.dangerSoftStrong
|
||||
if (root.danger && hover.containsMouse)
|
||||
return Theme.dangerSoft
|
||||
if (root.active)
|
||||
return Theme.accentSoft
|
||||
if (hover.pressed)
|
||||
return Theme.pressedFill
|
||||
if (hover.containsMouse)
|
||||
return Theme.hoverFill
|
||||
return "transparent"
|
||||
@@ -63,4 +80,9 @@ Item {
|
||||
FocusFrame {
|
||||
radius: Theme.radiusSmall
|
||||
}
|
||||
|
||||
NebulaToolTip {
|
||||
text: root.label
|
||||
active: hover.containsMouse && !hover.pressed && root.label.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property bool active: false
|
||||
property int delay: 550
|
||||
property int timeout: 4000
|
||||
|
||||
width: 0
|
||||
height: 0
|
||||
enabled: false
|
||||
Accessible.ignored: true
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: root.delay
|
||||
repeat: false
|
||||
onTriggered: root.open()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: root.timeout
|
||||
repeat: false
|
||||
onTriggered: tip.visible = false
|
||||
}
|
||||
|
||||
Window {
|
||||
id: tip
|
||||
flags: Qt.Popup | Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||
color: "transparent"
|
||||
visible: false
|
||||
width: Math.ceil(tipText.implicitWidth) + 16
|
||||
height: Math.ceil(tipText.implicitHeight) + 10
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.radiusSmall
|
||||
color: Theme.surfaceRaised
|
||||
border.color: Theme.borderStrong
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: tipText
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
color: Theme.textPrimary
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: root.sync()
|
||||
onTextChanged: {
|
||||
if (tip.visible)
|
||||
root.place()
|
||||
else
|
||||
root.sync()
|
||||
}
|
||||
|
||||
function sync() {
|
||||
if (root.active && root.text.length > 0) {
|
||||
showTimer.restart()
|
||||
return
|
||||
}
|
||||
showTimer.stop()
|
||||
hideTimer.stop()
|
||||
tip.visible = false
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!root.active || root.text.length === 0)
|
||||
return
|
||||
const host = root.parent
|
||||
if (!host)
|
||||
return
|
||||
const win = host.Window.window
|
||||
if (win)
|
||||
tip.transientParent = win
|
||||
root.place()
|
||||
tip.visible = true
|
||||
hideTimer.restart()
|
||||
}
|
||||
|
||||
function place() {
|
||||
const host = root.parent
|
||||
if (!host)
|
||||
return
|
||||
const pos = host.mapToGlobal(host.width / 2, host.height + 6)
|
||||
const screenWidth = Screen.width
|
||||
tip.x = Math.round(Math.max(8, Math.min(pos.x - tip.width / 2, screenWidth - tip.width - 8)))
|
||||
tip.y = Math.round(pos.y)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ QtObject {
|
||||
readonly property color focus: Qt.rgba(126 / 255, 160 / 255, 196 / 255, 0.55)
|
||||
|
||||
readonly property color hoverFill: Qt.rgba(1, 1, 1, 0.06)
|
||||
readonly property color pressedFill: Qt.rgba(1, 1, 1, 0.10)
|
||||
readonly property color dangerSoft: Qt.rgba(201 / 255, 136 / 255, 136 / 255, 0.18)
|
||||
readonly property color dangerSoftStrong: Qt.rgba(201 / 255, 136 / 255, 136 / 255, 0.28)
|
||||
readonly property color tileHover: Qt.rgba(1, 1, 1, 0.05)
|
||||
readonly property color iconWell: Qt.rgba(1, 1, 1, 0.05)
|
||||
readonly property color iconWellHighlight: Qt.rgba(1, 1, 1, 0.06)
|
||||
|
||||
@@ -48,6 +48,7 @@ set(NEBULA_SHELL_QML_FILES
|
||||
qml/surfaces/TopBarSurface.qml
|
||||
qml/surfaces/LauncherSurface.qml
|
||||
qml/components/TopBar.qml
|
||||
qml/components/WindowControls.qml
|
||||
qml/components/NebulaButton.qml
|
||||
qml/components/Clock.qml
|
||||
qml/components/SystemArea.qml
|
||||
|
||||
@@ -23,15 +23,19 @@ Item {
|
||||
id: left
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 6
|
||||
anchors.right: right.left
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
NebulaButton {
|
||||
id: nebulaButton
|
||||
checked: ShellState.launcherOpen
|
||||
onClicked: ShellState.toggleLauncher()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: leftDivider
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 1
|
||||
height: 14
|
||||
@@ -40,7 +44,7 @@ Item {
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, root.width * 0.4)
|
||||
width: Math.min(implicitWidth, Math.max(0, left.width - nebulaButton.width - leftDivider.width - left.spacing * 2))
|
||||
text: ShellState.activeApplication
|
||||
color: Theme.textSecondary
|
||||
font.family: Theme.fontFamily
|
||||
@@ -50,9 +54,28 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SystemArea {
|
||||
Row {
|
||||
id: right
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: windowControls.visible ? 8 : 0
|
||||
|
||||
WindowControls {
|
||||
id: windowControls
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: windowControls.visible
|
||||
width: visible ? 1 : 0
|
||||
height: 14
|
||||
color: Theme.borderStrong
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
SystemArea {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import QtQuick
|
||||
import Nebula.UI
|
||||
import Nebula.Windows
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool maximized: WindowService.focusedWindowMaximized
|
||||
|
||||
visible: WindowService.hasFocusedWindow
|
||||
implicitWidth: well.implicitWidth
|
||||
implicitHeight: Theme.controlHeight
|
||||
width: visible ? implicitWidth : 0
|
||||
Accessible.role: Accessible.Grouping
|
||||
Accessible.name: qsTr("Window controls")
|
||||
|
||||
Rectangle {
|
||||
id: well
|
||||
anchors.fill: parent
|
||||
implicitWidth: row.implicitWidth + 2
|
||||
implicitHeight: Theme.controlHeight
|
||||
radius: Theme.radiusSmall
|
||||
color: Theme.iconWell
|
||||
border.width: 1
|
||||
border.color: Theme.border
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
NebulaIconButton {
|
||||
id: minimizeButton
|
||||
width: 26
|
||||
height: 26
|
||||
label: qsTr("Minimise")
|
||||
onClicked: WindowService.minimizeFocusedWindow()
|
||||
|
||||
NebulaIcon {
|
||||
name: "minimize"
|
||||
size: 14
|
||||
color: minimizeButton.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
NebulaIconButton {
|
||||
id: maximizeButton
|
||||
width: 26
|
||||
height: 26
|
||||
label: root.maximized ? qsTr("Restore") : qsTr("Maximise")
|
||||
onClicked: WindowService.toggleMaximizeFocusedWindow()
|
||||
|
||||
NebulaIcon {
|
||||
name: root.maximized ? "restore" : "maximize"
|
||||
size: 14
|
||||
color: maximizeButton.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
NebulaIconButton {
|
||||
id: closeButton
|
||||
width: 26
|
||||
height: 26
|
||||
label: qsTr("Close")
|
||||
danger: true
|
||||
onClicked: WindowService.closeFocusedWindow()
|
||||
|
||||
NebulaIcon {
|
||||
name: "close"
|
||||
size: 14
|
||||
color: closeButton.iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user