├── .gitignore ├── README.md ├── filebrowser ├── .gitignore ├── .qmake.stash ├── filebrowser.pro ├── main.cpp ├── main.qml ├── qml.qrc └── qtquickcontrols2.conf └── webbrowser.qml /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qt client side window decorations demo 2 | 3 | Project to test the API for cross-platform client side window decorations in Qt which was introduced in Qt 5.15 ([QTBUG-73011](https://bugreports.qt.io/browse/QTBUG-73011)). 4 | 5 | See the [blog post](https://www.qt.io/blog/custom-window-decorations) I wrote for details. 6 | 7 | It should work on X11, Wayland, Windows and macOS (macOS has no resize support though). 8 | 9 | There are two demo applications in this project. 10 | 11 | The `filebrowser` demo should look something like this: 12 | 13 | ![file browser screenshot](https://i.imgur.com/8otn3Ng.png) 14 | 15 | The other demo `webbrowser.qml` is just a single file, and can just be run with 16 | 17 | ```sh 18 | $ qml webbrowser.qml 19 | ``` 20 | 21 | It should look like this: 22 | 23 | ![web browser screenshot](https://i.imgur.com/c8IParL.png) 24 | -------------------------------------------------------------------------------- /filebrowser/.gitignore: -------------------------------------------------------------------------------- 1 | filebrowser 2 | -------------------------------------------------------------------------------- /filebrowser/.qmake.stash: -------------------------------------------------------------------------------- 1 | QMAKE_CXX.QT_COMPILER_STDCXX = 201402L 2 | QMAKE_CXX.QMAKE_GCC_MAJOR_VERSION = 9 3 | QMAKE_CXX.QMAKE_GCC_MINOR_VERSION = 2 4 | QMAKE_CXX.QMAKE_GCC_PATCH_VERSION = 0 5 | QMAKE_CXX.COMPILER_MACROS = \ 6 | QT_COMPILER_STDCXX \ 7 | QMAKE_GCC_MAJOR_VERSION \ 8 | QMAKE_GCC_MINOR_VERSION \ 9 | QMAKE_GCC_PATCH_VERSION 10 | QMAKE_CXX.INCDIRS = \ 11 | /usr/include/c++/9.2.0 \ 12 | /usr/include/c++/9.2.0/x86_64-pc-linux-gnu \ 13 | /usr/include/c++/9.2.0/backward \ 14 | /usr/lib/gcc/x86_64-pc-linux-gnu/9.2.0/include \ 15 | /usr/local/include \ 16 | /usr/lib/gcc/x86_64-pc-linux-gnu/9.2.0/include-fixed \ 17 | /usr/include 18 | QMAKE_CXX.LIBDIRS = \ 19 | /usr/lib/gcc/x86_64-pc-linux-gnu/9.2.0 \ 20 | /usr/lib \ 21 | /lib 22 | -------------------------------------------------------------------------------- /filebrowser/filebrowser.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | QT += gui quick 3 | INCLUDEPATH += . 4 | DEFINES += QT_DEPRECATED_WARNINGS 5 | SOURCES += main.cpp 6 | RESOURCES += qml.qrc 7 | -------------------------------------------------------------------------------- /filebrowser/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 7 | 8 | QGuiApplication app(argc, argv); 9 | 10 | QQmlApplicationEngine engine; 11 | 12 | const QUrl mainQml(QStringLiteral("qrc:/main.qml")); 13 | 14 | // Catch the objectCreated signal, so that we can determine if the root component was loaded 15 | // successfully. If not, then the object created from it will be null. The root component may 16 | // get loaded asynchronously. 17 | const QMetaObject::Connection connection = QObject::connect( 18 | &engine, &QQmlApplicationEngine::objectCreated, 19 | &app, [&](QObject *object, const QUrl &url) { 20 | if (url != mainQml) 21 | return; 22 | 23 | if (!object) 24 | app.exit(-1); 25 | else 26 | QObject::disconnect(connection); 27 | }, Qt::QueuedConnection); 28 | 29 | engine.load(mainQml); 30 | 31 | return app.exec(); 32 | } 33 | -------------------------------------------------------------------------------- /filebrowser/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Shapes 1.12 3 | import QtQuick.Controls 2.3 4 | import QtQuick.Layouts 1.3 5 | import QtQuick.Window 2.3 6 | 7 | Window { 8 | id: window 9 | visible: true 10 | flags: Qt.FramelessWindowHint 11 | width: 640 12 | height: 480 13 | title: qsTr("Stack") 14 | color: "#99000000" 15 | 16 | function toggleMaximized() { 17 | if (window.visibility === Window.Maximized) { 18 | window.showNormal(); 19 | } else { 20 | window.showMaximized(); 21 | } 22 | } 23 | 24 | DragHandler { 25 | id: resizeHandler 26 | grabPermissions: TapHandler.TakeOverForbidden 27 | target: null 28 | onActiveChanged: if (active) { 29 | const p = resizeHandler.centroid.position; 30 | let e = 0; 31 | if (p.x / width < 0.10) { e |= Qt.LeftEdge } 32 | if (p.x / width > 0.90) { e |= Qt.RightEdge } 33 | if (p.y / height < 0.10) { e |= Qt.TopEdge } 34 | if (p.y / height > 0.90) { e |= Qt.BottomEdge } 35 | console.log("RESIZING", e); 36 | window.startSystemResize(e); 37 | } 38 | } 39 | 40 | Page { 41 | anchors.fill: parent 42 | anchors.margins: window.visibility === Window.Windowed ? 5 : 0 43 | header: ToolBar { 44 | contentHeight: toolButton.implicitHeight 45 | Item { 46 | anchors.fill: parent 47 | TapHandler { 48 | onTapped: if (tapCount === 2) toggleMaximized() 49 | gesturePolicy: TapHandler.DragThreshold 50 | } 51 | DragHandler { 52 | grabPermissions: TapHandler.CanTakeOverFromAnything 53 | onActiveChanged: if (active) { window.startSystemMove(); } 54 | } 55 | RowLayout { 56 | anchors.left: parent.left 57 | spacing: 3 58 | ToolButton { 59 | id: toolButton 60 | text: "\u2630" 61 | font.pixelSize: Qt.application.font.pixelSize * 1.6 62 | onClicked: drawer.open() 63 | } 64 | ToolButton { text: "home" } 65 | Label { text: "/" } 66 | ToolButton { text: "johan" } 67 | Label { text: "/" } 68 | ToolButton { text: "dev" } 69 | } 70 | 71 | RowLayout { 72 | spacing: 0 73 | anchors.right: parent.right 74 | anchors.rightMargin: 5 75 | TextField { 76 | placeholderText: "search" 77 | } 78 | ToolButton { 79 | text: "🗕" 80 | font.pixelSize: Qt.application.font.pixelSize * 1.6 81 | onClicked: window.showMinimized(); 82 | } 83 | ToolButton { 84 | text: window.visibility == Window.Maximized ? "🗗" : "🗖" 85 | font.pixelSize: Qt.application.font.pixelSize * 1.6 86 | onClicked: window.toggleMaximized() 87 | } 88 | ToolButton { 89 | text: "🗙" 90 | font.pixelSize: Qt.application.font.pixelSize * 1.6 91 | onClicked: window.close() 92 | } 93 | } 94 | } 95 | } 96 | 97 | Drawer { 98 | id: drawer 99 | width: window.width * 0.66 100 | height: window.height 101 | interactive: window.visibility !== Window.Windowed || position > 0 102 | } 103 | 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /filebrowser/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | qtquickcontrols2.conf 5 | 6 | 7 | -------------------------------------------------------------------------------- /filebrowser/qtquickcontrols2.conf: -------------------------------------------------------------------------------- 1 | ; This file can be edited to change the style of the application 2 | ; Read "Qt Quick Controls 2 Configuration File" for details: 3 | ; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html 4 | 5 | [Controls] 6 | Style=Material 7 | 8 | [Material] 9 | Theme=Dark 10 | ;Accent=BlueGrey 11 | ;Primary=BlueGray 12 | ;Foreground=Brown 13 | ;Background=Grey 14 | -------------------------------------------------------------------------------- /webbrowser.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Shapes 1.12 3 | import QtQuick.Controls 2.3 4 | import QtQuick.Layouts 1.3 5 | import QtQuick.Window 2.3 6 | 7 | Window { 8 | id: window 9 | visible: true 10 | flags: Qt.FramelessWindowHint 11 | width: 640 12 | height: 480 13 | title: qsTr("Stack") 14 | color: "#99000000" 15 | property int bw: 5 16 | 17 | function toggleMaximized() { 18 | if (window.visibility === Window.Maximized) { 19 | window.showNormal(); 20 | } else { 21 | window.showMaximized(); 22 | } 23 | } 24 | 25 | // The mouse area is just for setting the right cursor shape 26 | MouseArea { 27 | anchors.fill: parent 28 | hoverEnabled: true 29 | cursorShape: { 30 | const p = Qt.point(mouseX, mouseY); 31 | const b = bw + 10; // Increase the corner size slightly 32 | if (p.x < b && p.y < b) return Qt.SizeFDiagCursor; 33 | if (p.x >= width - b && p.y >= height - b) return Qt.SizeFDiagCursor; 34 | if (p.x >= width - b && p.y < b) return Qt.SizeBDiagCursor; 35 | if (p.x < b && p.y >= height - b) return Qt.SizeBDiagCursor; 36 | if (p.x < b || p.x >= width - b) return Qt.SizeHorCursor; 37 | if (p.y < b || p.y >= height - b) return Qt.SizeVerCursor; 38 | } 39 | acceptedButtons: Qt.NoButton // don't handle actual events 40 | } 41 | 42 | DragHandler { 43 | id: resizeHandler 44 | grabPermissions: TapHandler.TakeOverForbidden 45 | target: null 46 | onActiveChanged: if (active) { 47 | const p = resizeHandler.centroid.position; 48 | const b = bw + 10; // Increase the corner size slightly 49 | let e = 0; 50 | if (p.x < b) { e |= Qt.LeftEdge } 51 | if (p.x >= width - b) { e |= Qt.RightEdge } 52 | if (p.y < b) { e |= Qt.TopEdge } 53 | if (p.y >= height - b) { e |= Qt.BottomEdge } 54 | window.startSystemResize(e); 55 | } 56 | } 57 | 58 | Page { 59 | anchors.fill: parent 60 | anchors.margins: window.visibility === Window.Windowed ? bw : 0 61 | // footer: ToolBar { 62 | header: ToolBar { 63 | contentHeight: toolButton.implicitHeight 64 | Item { 65 | anchors.fill: parent 66 | TapHandler { 67 | onTapped: if (tapCount === 2) toggleMaximized() 68 | gesturePolicy: TapHandler.DragThreshold 69 | } 70 | DragHandler { 71 | grabPermissions: TapHandler.CanTakeOverFromAnything 72 | onActiveChanged: if (active) { window.startSystemMove(); } 73 | } 74 | RowLayout { 75 | anchors.left: parent.left 76 | anchors.right: parent.right 77 | spacing: 3 78 | Layout.fillWidth: true 79 | TabBar { 80 | spacing: 0 81 | Repeater { 82 | model: ["Google", "GitHub - johanhelsing/qt-csd-demo", "Unicode: Arrows"] 83 | TabButton { 84 | id: tab 85 | implicitWidth: 150 86 | text: modelData 87 | padding: 0 88 | contentItem: Item { 89 | implicitWidth: 120 90 | implicitHeight: 20 91 | clip: true 92 | Label { 93 | id: tabIcon 94 | text: "↻" 95 | anchors.top: parent.top 96 | anchors.bottom: parent.bottom 97 | horizontalAlignment: Text.AlignHCenter 98 | verticalAlignment: Text.AlignVCenter 99 | width: 30 100 | } 101 | Text { 102 | anchors.left: tabIcon.right 103 | anchors.top: parent.top 104 | anchors.bottom: parent.bottom 105 | text: tab.text 106 | font: tab.font 107 | opacity: enabled ? 1.0 : 0.3 108 | horizontalAlignment: Text.AlignHCenter 109 | verticalAlignment: Text.AlignVCenter 110 | elide: Text.ElideRight 111 | } 112 | Rectangle { 113 | anchors.top: parent.top 114 | anchors.bottom: parent.bottom 115 | anchors.right: tab.checked ? closeButton.left : parent.right 116 | width: 20 117 | gradient: Gradient { 118 | orientation: Gradient.Horizontal 119 | GradientStop { position: 0; color: "transparent" } 120 | //GradientStop { position: 1; color: palette.button } 121 | GradientStop { position: 0.7; color: tab.background.color } 122 | } 123 | } 124 | Button { 125 | id: closeButton 126 | anchors.right: parent.right 127 | anchors.bottom: parent.bottom 128 | anchors.top: parent.top 129 | visible: tab.checked 130 | text: "🗙" 131 | contentItem: Text { 132 | text: closeButton.text 133 | font: closeButton.font 134 | opacity: enabled ? 1.0 : 0.3 135 | color: "black" 136 | horizontalAlignment: Text.AlignHCenter 137 | verticalAlignment: Text.AlignVCenter 138 | elide: Text.ElideRight 139 | } 140 | background: Rectangle { 141 | implicitWidth: 10 142 | implicitHeight: 10 143 | opacity: enabled ? 1 : 0.3 144 | color: tab.background.color 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | RowLayout { 152 | spacing: 0 153 | ToolButton { text: "+" } 154 | Item { Layout.fillWidth: true } 155 | ToolButton { 156 | text: "🗕" 157 | onClicked: window.showMinimized(); 158 | } 159 | ToolButton { 160 | text: window.visibility === Window.Maximized ? "🗗" : "🗖" 161 | onClicked: window.toggleMaximized() 162 | } 163 | ToolButton { 164 | text: "🗙" 165 | onClicked: window.close() 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | Page { 173 | anchors.fill: parent 174 | header: ToolBar { 175 | RowLayout { 176 | spacing: 0 177 | anchors.fill: parent 178 | ToolButton { text: "←" } 179 | ToolButton { text: "→" } 180 | ToolButton { text: "↻" } 181 | TextField { 182 | text: "https://google.com" 183 | Layout.fillWidth: true 184 | } 185 | ToolButton { 186 | id: toolButton 187 | text: "\u2630" 188 | onClicked: drawer.open() 189 | } 190 | } 191 | } 192 | } 193 | 194 | Drawer { 195 | id: drawer 196 | width: window.width * 0.66 197 | height: window.height 198 | edge: Qt.RightEdge 199 | interactive: window.visibility !== Window.Windowed || position > 0 200 | } 201 | 202 | } 203 | } 204 | --------------------------------------------------------------------------------