From 3d34ce7f76e7cb762d39e646b9059a869fd1c527 Mon Sep 17 00:00:00 2001 From: Filippo Scognamiglio Date: Thu, 15 Jan 2026 23:02:38 +0100 Subject: [PATCH] Tentative first implementation of client side decorations. --- app/qml/TerminalTabs.qml | 73 ++---------- app/qml/TerminalTabsBar.qml | 217 ++++++++++++++++++++++++++++++++++++ app/qml/TerminalWindow.qml | 100 ++++++++++++++++- app/qml/main.qml | 11 +- app/qml/resources.qrc | 1 + qmltermwidget | 2 +- 6 files changed, 328 insertions(+), 76 deletions(-) create mode 100644 app/qml/TerminalTabsBar.qml diff --git a/app/qml/TerminalTabs.qml b/app/qml/TerminalTabs.qml index 361a172..9a26886 100644 --- a/app/qml/TerminalTabs.qml +++ b/app/qml/TerminalTabs.qml @@ -18,18 +18,19 @@ * along with this program. If not, see . *******************************************************************************/ import QtQuick -import QtQuick.Controls import QtQuick.Layouts Item { id: tabsRoot - readonly property int innerPadding: 6 - readonly property string currentTitle: tabsModel.get(currentIndex).title ?? "cool-retro-term" + readonly property string currentTitle: tabsModel.count > 0 + ? (tabsModel.get(currentIndex).title ?? "cool-retro-term") + : "cool-retro-term" readonly property size terminalSize: stack.currentItem ? stack.currentItem.terminalSize : Qt.size(0, 0) - property alias currentIndex: tabBar.currentIndex + property int currentIndex: 0 readonly property int count: tabsModel.count property var hostWindow + property alias tabsModel: tabsModel function normalizeTitle(rawTitle) { if (rawTitle === undefined || rawTitle === null) { @@ -40,7 +41,7 @@ Item { function addTab() { tabsModel.append({ title: "" }) - tabBar.currentIndex = tabsModel.count - 1 + currentIndex = tabsModel.count - 1 } function closeTab(index) { @@ -50,7 +51,7 @@ Item { } tabsModel.remove(index) - tabBar.currentIndex = Math.min(tabBar.currentIndex, tabsModel.count - 1) + currentIndex = Math.min(currentIndex, tabsModel.count - 1) } ListModel { @@ -63,69 +64,11 @@ Item { anchors.fill: parent spacing: 0 - Rectangle { - id: tabRow - Layout.fillWidth: true - height: rowLayout.implicitHeight - color: palette.window - visible: tabsModel.count > 1 - - RowLayout { - id: rowLayout - anchors.fill: parent - spacing: 0 - - TabBar { - id: tabBar - Layout.fillWidth: true - Layout.fillHeight: true - focusPolicy: Qt.NoFocus - - Repeater { - model: tabsModel - TabButton { - id: tabButton - contentItem: RowLayout { - anchors.fill: parent - anchors { leftMargin: innerPadding; rightMargin: innerPadding } - spacing: innerPadding - - Label { - text: model.title - elide: Text.ElideRight - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - - ToolButton { - text: "\u00d7" - focusPolicy: Qt.NoFocus - padding: innerPadding - Layout.alignment: Qt.AlignVCenter - onClicked: tabsRoot.closeTab(index) - } - } - } - } - } - - ToolButton { - id: addTabButton - text: "+" - focusPolicy: Qt.NoFocus - Layout.fillHeight: true - padding: innerPadding - Layout.alignment: Qt.AlignVCenter - onClicked: tabsRoot.addTab() - } - } - } - StackLayout { id: stack Layout.fillWidth: true Layout.fillHeight: true - currentIndex: tabBar.currentIndex + currentIndex: tabsRoot.currentIndex Repeater { model: tabsModel diff --git a/app/qml/TerminalTabsBar.qml b/app/qml/TerminalTabsBar.qml new file mode 100644 index 0000000..cf08eeb --- /dev/null +++ b/app/qml/TerminalTabsBar.qml @@ -0,0 +1,217 @@ +/******************************************************************************* +* Copyright (c) 2013-2021 "Filippo Scognamiglio" +* https://github.com/Swordfish90/cool-retro-term +* +* This file is part of cool-retro-term. +* +* cool-retro-term 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 3 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, see . +*******************************************************************************/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window + +Item { + id: barRoot + + readonly property int innerPadding: 6 + readonly property int leftInset: (isMacOS && !showWindowControls) ? 72 : 0 + property var tabsController + property var hostWindow + property bool isMacOS: false + property bool showWindowControls: true + property bool windowControlsOnLeft: false + property bool enableSystemMove: true + property bool enableDoubleClickMaximize: true + + implicitHeight: rowLayout.implicitHeight + + function toggleMaximize() { + if (!hostWindow) { + return + } + hostWindow.visibility = (hostWindow.visibility === Window.Maximized) + ? Window.Windowed + : Window.Maximized + } + + onTabsControllerChanged: { + if (tabsController) { + tabBar.currentIndex = tabsController.currentIndex + } + } + + Component.onCompleted: { + if (tabsController) { + tabBar.currentIndex = tabsController.currentIndex + } + } + + Rectangle { + anchors.fill: parent + color: palette.window + } + + RowLayout { + id: rowLayout + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillHeight: true + Layout.preferredWidth: leftInset + visible: leftInset > 0 + } + + Loader { + active: showWindowControls && windowControlsOnLeft + sourceComponent: windowControlsComponent + } + + TabBar { + id: tabBar + Layout.fillWidth: true + Layout.fillHeight: true + focusPolicy: Qt.NoFocus + + onCurrentIndexChanged: { + if (tabsController && tabsController.currentIndex !== currentIndex) { + tabsController.currentIndex = currentIndex + } + } + + Repeater { + model: tabsController ? tabsController.tabsModel : null + TabButton { + id: tabButton + contentItem: RowLayout { + anchors.fill: parent + anchors { leftMargin: innerPadding; rightMargin: innerPadding } + spacing: innerPadding + + Label { + text: model.title + elide: Text.ElideRight + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + ToolButton { + text: "\u00d7" + focusPolicy: Qt.NoFocus + padding: innerPadding + Layout.alignment: Qt.AlignVCenter + onClicked: { + if (tabsController) { + tabsController.closeTab(index) + } + } + } + } + } + } + } + + ToolButton { + id: addTabButton + text: "+" + focusPolicy: Qt.NoFocus + Layout.fillHeight: true + padding: innerPadding + Layout.alignment: Qt.AlignVCenter + onClicked: { + if (tabsController) { + tabsController.addTab() + } + } + } + + Loader { + active: showWindowControls && !windowControlsOnLeft + sourceComponent: windowControlsComponent + } + } + + Component { + id: windowControlsComponent + RowLayout { + id: windowControls + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + spacing: 0 + + ToolButton { + text: "\u2212" + focusPolicy: Qt.NoFocus + padding: innerPadding + Layout.alignment: Qt.AlignVCenter + onClicked: { + if (hostWindow) { + hostWindow.visibility = Window.Minimized + } + } + } + + ToolButton { + text: hostWindow && hostWindow.visibility === Window.Maximized ? "\u2752" : "\u25a1" + focusPolicy: Qt.NoFocus + padding: innerPadding + Layout.alignment: Qt.AlignVCenter + onClicked: toggleMaximize() + } + + ToolButton { + text: "\u00d7" + focusPolicy: Qt.NoFocus + padding: innerPadding + Layout.alignment: Qt.AlignVCenter + onClicked: { + if (hostWindow) { + hostWindow.close() + } + } + } + } + } + + Connections { + target: tabsController + function onCurrentIndexChanged() { + if (tabBar.currentIndex !== tabsController.currentIndex) { + tabBar.currentIndex = tabsController.currentIndex + } + } + } + + DragHandler { + acceptedDevices: PointerDevice.Mouse + acceptedButtons: Qt.LeftButton + grabPermissions: PointerHandler.CanTakeOverFromItems + target: null + onActiveChanged: { + if (active && hostWindow && enableSystemMove) { + hostWindow.startSystemMove() + } + } + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: { + if (tapCount === 2 && enableDoubleClickMaximize) { + toggleMaximize() + } + } + } +} diff --git a/app/qml/TerminalWindow.qml b/app/qml/TerminalWindow.qml index 232c4aa..dda4e42 100644 --- a/app/qml/TerminalWindow.qml +++ b/app/qml/TerminalWindow.qml @@ -20,6 +20,7 @@ import QtQuick 2.2 import QtQuick.Window 2.1 import QtQuick.Controls 2.3 +import QtQuick.Layouts import "menus" @@ -39,6 +40,8 @@ ApplicationWindow { visible: false + flags: Qt.Window | Qt.FramelessWindowHint + property bool fullscreen: appSettings.fullscreen onFullscreenChanged: visibility = (fullscreen ? Window.FullScreen : Window.Windowed) @@ -137,11 +140,27 @@ ApplicationWindow { text: qsTr("New Tab") onTriggered: terminalTabs.addTab() } - TerminalTabs { - id: terminalTabs - width: parent.width - height: (parent.height + Math.abs(y)) - hostWindow: terminalWindow + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TerminalTabsBar { + Layout.fillWidth: true + tabsController: terminalTabs + hostWindow: terminalWindow + isMacOS: appSettings.isMacOS + showWindowControls: true + windowControlsOnLeft: appSettings.isMacOS + enableSystemMove: true + enableDoubleClickMaximize: true + } + + TerminalTabs { + id: terminalTabs + Layout.fillWidth: true + Layout.fillHeight: true + hostWindow: terminalWindow + } } Loader { anchors.centerIn: parent @@ -151,6 +170,77 @@ ApplicationWindow { terminalSize: terminalTabs.terminalSize } } + + Item { + id: resizeHandles + anchors.fill: parent + visible: true + property int resizeMargin: 6 + + MouseArea { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: resizeHandles.resizeMargin + cursorShape: Qt.SizeHorCursor + onPressed: terminalWindow.startSystemResize(Qt.LeftEdge) + } + + MouseArea { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: resizeHandles.resizeMargin + cursorShape: Qt.SizeHorCursor + onPressed: terminalWindow.startSystemResize(Qt.RightEdge) + } + + MouseArea { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: resizeHandles.resizeMargin + cursorShape: Qt.SizeVerCursor + onPressed: terminalWindow.startSystemResize(Qt.BottomEdge) + } + + MouseArea { + anchors.left: parent.left + anchors.top: parent.top + width: resizeHandles.resizeMargin + height: resizeHandles.resizeMargin + cursorShape: Qt.SizeFDiagCursor + onPressed: terminalWindow.startSystemResize(Qt.TopEdge | Qt.LeftEdge) + } + + MouseArea { + anchors.right: parent.right + anchors.top: parent.top + width: resizeHandles.resizeMargin + height: resizeHandles.resizeMargin + cursorShape: Qt.SizeBDiagCursor + onPressed: terminalWindow.startSystemResize(Qt.TopEdge | Qt.RightEdge) + } + + MouseArea { + anchors.left: parent.left + anchors.bottom: parent.bottom + width: resizeHandles.resizeMargin + height: resizeHandles.resizeMargin + cursorShape: Qt.SizeBDiagCursor + onPressed: terminalWindow.startSystemResize(Qt.BottomEdge | Qt.LeftEdge) + } + + MouseArea { + anchors.right: parent.right + anchors.bottom: parent.bottom + width: resizeHandles.resizeMargin + height: resizeHandles.resizeMargin + cursorShape: Qt.SizeFDiagCursor + onPressed: terminalWindow.startSystemResize(Qt.BottomEdge | Qt.RightEdge) + } + } + onClosing: { appRoot.closeWindow(terminalWindow) } diff --git a/app/qml/main.qml b/app/qml/main.qml index a0e7eaa..0f96190 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -38,14 +38,15 @@ QtObject { visible: false } - property Component windowComponent: Component { - TerminalWindow { } - } - property ListModel windowsModel: ListModel { } function createWindow() { - var window = windowComponent.createObject(null) + var component = Qt.createComponent("TerminalWindow.qml") + if (component.status === Component.Error) { + console.error(component.errorString()) + return + } + var window = component.createObject(null) if (!window) return diff --git a/app/qml/resources.qrc b/app/qml/resources.qrc index 961600e..886897c 100644 --- a/app/qml/resources.qrc +++ b/app/qml/resources.qrc @@ -22,6 +22,7 @@ SettingsAdvancedTab.qml TerminalContainer.qml TerminalTabs.qml + TerminalTabsBar.qml images/crt256.png utils.js images/allNoise512.png diff --git a/qmltermwidget b/qmltermwidget index a3822c5..7a5f3b6 160000 --- a/qmltermwidget +++ b/qmltermwidget @@ -1 +1 @@ -Subproject commit a3822c5de93bd72138fbb5d606da96cb1f7e79f0 +Subproject commit 7a5f3b68a58bab963ac97a8cde0d13bfe0ae4d0b