Thermostat: Rework layout + general cleanup

-Define sane minimum sizes
-Rework layouts to have well defined sizes and be mutually exclusive.
 The small desktop layout now stats at the 1024x768 resolution. Adapt
 existing elements to display correctly on the smallDesktopLayout.
-Make general improvements throughout the app in terms of layout,
 padding, alignment, ...

These changes ensure that the app has a usable UI at every resolution
above the minimum size. It updates its layout dynamically from a small
resolution to a big desktop resolution while also handling mobile aspect
ratios. One extra "Tablet" layout would probably improve usability for
medium resoltions where the app currently struggles to keep an ergonomic
interface.

Fixes: QTBUG-140396
Pick-to: 6.10
Change-Id: Ibd8bce01a38508e61686ca875f2d2a79fb156b6f
Reviewed-by: Sami Shalayel <sami.shalayel@qt.io>
This commit is contained in:
Olivier De Cannière 2025-09-22 18:44:18 +02:00
parent 07171a70ff
commit 7e0155cc36
18 changed files with 231 additions and 162 deletions

View File

@ -7,11 +7,11 @@ import Thermostat
Window {
id: window
width: Constants.width
height: Constants.height
width: 1440
height: 1080
minimumHeight: 272
minimumWidth: Qt.PrimaryOrientation === Qt.LandscapeOrientation ? 480 : 360
minimumHeight: 306
minimumWidth: Constants.isMobileLayout ? 364 : 484
visible: true
title: "Thermostat"
@ -22,17 +22,15 @@ Window {
}
Component.onCompleted: {
Constants.isBigDesktopLayout = Qt.binding( function(){
return window.width >= Constants.width && window.width >= window.height
})
Constants.isSmallDesktopLayout = Qt.binding( function(){
return window.width >= 647 && window.width < Constants.width && window.width >= window.height
})
Constants.isMobileLayout = Qt.binding( function(){
return window.width < window.height
})
Constants.isSmallLayout = Qt.binding( function(){
return window.width < 647 && window.width >= window.height
Constants.layout = Qt.binding(() => {
let tall = window.height >= window.width
if (window.width >= 1440 && window.height >= 520)
return Constants.Layout.Desktop
if (window.width >= 1024 && window.height >= 768)
return Constants.Layout.SmallDesktop
if (tall || (window.width >= 600 && window.height >= 380))
return Constants.Layout.Mobile
return Constants.Layout.Small
})
}
}

View File

@ -19,6 +19,7 @@ ScrollView {
id: scrollView
clip: true
bottomPadding: 10
contentWidth: availableWidth
required property list<Room> roomsList

View File

@ -17,11 +17,6 @@ import ThermostatCustomControls
Pane {
id: root
topPadding: 4
leftPadding: 27
rightPadding: 27
bottomPadding: 13
required property list<Room> roomsList
background: Rectangle {

View File

@ -24,7 +24,8 @@ ScrollView {
property int tempSetterWidth: 1087
clip: true
padding: 0
bottomPadding: 10
contentWidth: availableWidth
background: Rectangle {
color: Constants.accentColor
@ -33,37 +34,55 @@ ScrollView {
}
ColumnLayout {
width: scrollView.width
height: scrollView.height
id: layout
anchors.horizontalCenter: parent.horizontalCenter
TimeSchedule {
id: timeSchedule
Layout.preferredHeight: scrollView.timeScheduleHeight
Layout.preferredWidth: scrollView.timeScheduleWidth
Layout.alignment: Qt.AlignHCenter
scheduleViewRoot: scrollView.scheduleViewRoot
}
TemperatureSetter {
Layout.preferredHeight: scrollView.tempSetterHeight
Layout.preferredWidth: scrollView.tempSetterWidth
Layout.alignment: Qt.AlignHCenter
scheduleViewRoot: scrollView.scheduleViewRoot
}
}
states: [
State {
name: "desktopLayout"
when: Constants.isBigDesktopLayout || Constants.isSmallDesktopLayout
name: "bigDesktopLayout"
when: Constants.isBigDesktopLayout
PropertyChanges {
target: scrollView
timeScheduleHeight: 361
timeScheduleWidth: 1087
tempSetterHeight: 427
tempSetterWidth: 1087
timeScheduleWidth: layout.width
tempSetterHeight: 350
tempSetterWidth: layout.width
isBackgroundVisible: false
}
PropertyChanges {
target: layout
width: 1100
}
},
State {
name: "smallDesktopLayout"
when: Constants.isSmallDesktopLayout
PropertyChanges {
target: scrollView
timeScheduleHeight: 361
timeScheduleWidth: layout.width
tempSetterHeight: 350
tempSetterWidth: layout.width
isBackgroundVisible: false
}
PropertyChanges {
target: layout
width: 918
}
},
State {
name: "mobileLayout"
@ -71,11 +90,15 @@ ScrollView {
PropertyChanges {
target: scrollView
timeScheduleHeight: 314
timeScheduleWidth: 327
timeScheduleWidth: layout.width
tempSetterHeight: 450
tempSetterWidth: 327
tempSetterWidth: layout.width
isBackgroundVisible: false
}
PropertyChanges {
target: layout
width: 334
}
},
State {
name: "smallLayout"
@ -83,11 +106,15 @@ ScrollView {
PropertyChanges {
target: scrollView
timeScheduleHeight: 230
timeScheduleWidth: 400
timeScheduleWidth: layout.width
tempSetterHeight: 300
tempSetterWidth: 400
tempSetterWidth: layout.width
isBackgroundVisible: true
}
PropertyChanges {
target: layout
width: 427
}
}
]
}

View File

@ -27,7 +27,7 @@ ScrollView {
property int delegateWidth: 350
clip: true
padding: 0
bottomPadding: 10
contentWidth: availableWidth
background: Rectangle {
@ -41,6 +41,7 @@ ScrollView {
width: scrollView.width
height: scrollView.height
anchors.horizontalCenter: parent.horizontalCenter
columns: scrollView.isOneColumn ? 1 : 3
rows: scrollView.isOneColumn ? 8 : 1
@ -51,11 +52,6 @@ ScrollView {
Pane {
id: statistics
leftPadding: 53
rightPadding: 53
topPadding: 23
bottomPadding: 43
Layout.columnSpan: scrollView.isOneColumn ? 1 : 3
Layout.rowSpan: scrollView.isOneColumn ? 5 : 1
@ -72,6 +68,7 @@ ScrollView {
id: statisticsChart
anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter
energyValues: scrollView.room.energyStats
tempValues: scrollView.room.tempStats
}
@ -107,8 +104,8 @@ ScrollView {
states: [
State {
name: "desktopLayout"
when: Constants.isBigDesktopLayout || Constants.isSmallDesktopLayout
name: "bigDesktopLayout"
when: Constants.isBigDesktopLayout
PropertyChanges {
target: statistics
leftPadding: 53
@ -121,9 +118,36 @@ ScrollView {
isBackgroundVisible: false
delegateWidth: 350
delegateHeight: 182
statisticsChartWidth: 1098
statisticsChartWidth: grid.width
statisticsChartHeight: 647
}
PropertyChanges {
target: grid
width: 1100
}
},
State {
name: "smallDesktopLayout"
when: Constants.isSmallDesktopLayout
PropertyChanges {
target: statistics
leftPadding: 53
rightPadding: 53
topPadding: 23
bottomPadding: 43
}
PropertyChanges {
target: scrollView
isBackgroundVisible: false
delegateWidth: 290
delegateHeight: 182
statisticsChartWidth: grid.width
statisticsChartHeight: 541
}
PropertyChanges {
target: grid
width: 918
}
},
State {
name: "mobileLayout"
@ -133,14 +157,14 @@ ScrollView {
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 43
bottomPadding: 0
}
PropertyChanges {
target: scrollView
isBackgroundVisible: false
delegateWidth: 327
delegateHeight: 100
statisticsChartWidth: 327
delegateHeight: 110
statisticsChartWidth: 346
statisticsChartHeight: 383
}
},
@ -152,15 +176,15 @@ ScrollView {
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 43
bottomPadding: 0
}
PropertyChanges {
target: scrollView
isBackgroundVisible: true
delegateWidth: 332
delegateHeight: 80
statisticsChartWidth: 401
statisticsChartHeight: 280
delegateHeight: 90
statisticsChartWidth: 420
statisticsChartHeight: 240
}
}
]

View File

@ -72,8 +72,6 @@ Pane {
id: scrollView
anchors.top: title.bottom
anchors.topMargin: 12
anchors.leftMargin: 28
width: internal.contentWidth
height: internal.contentHeight
@ -109,7 +107,7 @@ Pane {
readonly property int contentHeight: root.height - title.height
- root.topPadding - root.bottomPadding
readonly property int contentWidth: root.width - root.rightPadding - root.leftPadding
readonly property bool isOneColumn: contentWidth < 900
readonly property bool isOneColumn: Constants.isSmallLayout || Constants.isMobileLayout
}
states: [
@ -177,12 +175,6 @@ Pane {
target: label
visible: false
}
PropertyChanges {
target: root
leftPadding: 15
rightPadding: 15
topPadding: 3
}
PropertyChanges {
target: scrollView
visible: false

View File

@ -23,13 +23,9 @@ Pane {
property alias cancelButton: cancelButton
property int currentMode: 2
width: 1087
height: 427
topPadding: 30
bottomPadding: 24
leftPadding: 16
rightPadding: 36
width: 1010
height: 330
padding: 0
background: Rectangle {
color: Constants.accentColor
@ -38,9 +34,11 @@ Pane {
Row {
id: row1
width: parent.width
width: root.width
height: 70
spacing: 80
anchors.topMargin: 20
anchors.leftMargin: 20
Row {
anchors.verticalCenter: parent.verticalCenter
@ -89,6 +87,7 @@ Pane {
id: slider
value: root.scheduleViewRoot.currentTemp
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
Connections {
function onValueChanged() {
root.scheduleViewRoot.currentTemp = slider.value
@ -98,11 +97,14 @@ Pane {
}
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.top: row1.bottom
anchors.left: parent.left
anchors.topMargin: 20
anchors.leftMargin: 20
spacing: 50
Row {
id: row
spacing: 70
spacing: 60
Label {
font.pixelSize: 24
font.weight: 600
@ -153,7 +155,7 @@ Pane {
required property string modelData
text: modelData
width: 90
width: Constants.isSmallDesktopLayout ? 80 : 90
height: 50
radius: 12
font.pixelSize: 24
@ -167,41 +169,40 @@ Pane {
}
}
}
}
Row {
anchors.bottom: parent.bottom
anchors.right: parent.right
spacing: 24
Row {
anchors.right: parent.right
anchors.rightMargin: 20
spacing: 24
CustomRoundButton {
id: cancelButton
CustomRoundButton {
id: cancelButton
width: 120
height: 48
text: qsTr("Cancel")
radius: 12
contentColor: "#2CDE85"
checkable: false
font.pixelSize: 14
}
width: 120
height: 48
text: qsTr("Cancel")
radius: 12
contentColor: "#2CDE85"
checkable: false
font.pixelSize: 14
}
CustomRoundButton {
id: saveButton
width: 120
height: 48
text: qsTr("Save")
radius: 12
contentColor: "#2CDE85"
checkable: false
font.pixelSize: 14
CustomRoundButton {
id: saveButton
width: 120
height: 48
text: qsTr("Save")
radius: 12
contentColor: "#2CDE85"
checkable: false
font.pixelSize: 14
}
}
}
QtObject {
id: internal
property int fontSize: 24
property int topMargin: 67
property int iconSize: 34
}
}

View File

@ -34,6 +34,7 @@ Pane {
TemperatureSetterDesktopView {
id: desktopView
scheduleViewRoot: root.scheduleViewRoot
anchors.fill: parent
}
TemperatureSetterMobileView {

View File

@ -27,8 +27,7 @@ Pane {
width: 329
height: 430
topPadding: 16
bottomPadding: 19
bottomPadding: 10
background: Rectangle {
color: Constants.accentColor

View File

@ -102,42 +102,39 @@ Pane {
color: Constants.primaryTextColor
}
RowLayout {
spacing: 32
Row {
spacing: 10
Row {
spacing: 10
Image {
id: bottomLeftIcon
}
Label {
id: bottomLeftLabel
text: qsTr("Night: 25°C (10pm-6am)")
font.pixelSize: internal.infoFontSize
font.weight: 600
font.family: "Titillium Web"
color: Constants.primaryTextColor
}
Image {
id: bottomLeftIcon
}
Row {
spacing: 10
Label {
id: bottomLeftLabel
Image {
id: bottomRightIcon
}
text: qsTr("Night: 25°C (10pm-6am)")
font.pixelSize: internal.infoFontSize
font.weight: 600
font.family: "Titillium Web"
color: Constants.primaryTextColor
}
}
Label {
id: bottomRightLabel
Row {
spacing: 10
font.pixelSize: internal.infoFontSize
font.weight: 600
font.family: "Titillium Web"
color: Constants.primaryTextColor
}
Image {
id: bottomRightIcon
}
Label {
id: bottomRightLabel
font.pixelSize: internal.infoFontSize
font.weight: 600
font.family: "Titillium Web"
color: Constants.primaryTextColor
}
}
}

View File

@ -17,6 +17,7 @@ ScrollView {
id: root
clip: true
bottomPadding: 10
contentWidth: availableWidth
required property Room room
@ -37,6 +38,7 @@ ScrollView {
width: root.width
height: root.height
anchors.horizontalCenter: parent.horizontalCenter
columns: root.isOneColumn ? 1 : 3
rows: root.isOneColumn ? 10 : 1
columnSpacing: 24
@ -88,15 +90,34 @@ ScrollView {
states: [
State {
name: "desktopLayout"
when: Constants.isBigDesktopLayout || Constants.isSmallDesktopLayout
name: "bigDesktopLayout"
when: Constants.isBigDesktopLayout
PropertyChanges {
target: root
thermostatControlHeight: 673
thermostatControlWidth: 1094
thermostatControlWidth: grid.width
delegateHeight: 182
delegateWidth: 350
}
PropertyChanges {
target: grid
width: 1100
}
},
State {
name: "smallDesktopLayout"
when: Constants.isSmallDesktopLayout
PropertyChanges {
target: root
thermostatControlHeight: 673
thermostatControlWidth: grid.width
delegateHeight: 182
delegateWidth: 290
}
PropertyChanges {
target: grid
width: 918
}
},
State {
name: "mobileLayout"
@ -104,7 +125,7 @@ ScrollView {
PropertyChanges {
target: root
delegateWidth: 327
delegateHeight: 100
delegateHeight: 120
thermostatControlHeight: 694
thermostatControlWidth: 327
}
@ -115,7 +136,7 @@ ScrollView {
PropertyChanges {
target: root
delegateWidth: 332
delegateHeight: 80
delegateHeight: 90
thermostatControlHeight: 250
thermostatControlWidth: 400
}

View File

@ -109,7 +109,7 @@ Pane {
Column {
id: leftButtons
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: thermostat.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 100
spacing: 25
@ -137,7 +137,7 @@ Pane {
Column {
id: rightButtons
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: thermostat.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 100
spacing: 25
@ -214,7 +214,7 @@ Pane {
optionIconSize: 42
headerSpacing: 24
headerSize: 24
thermostatTopMargin: 14
thermostatTopMargin: 60
}
PropertyChanges {
target: powerButton
@ -230,6 +230,14 @@ Pane {
target: powerToggle
visible: false
}
PropertyChanges {
target: leftButtons
anchors.leftMargin: Constants.isSmallDesktopLayout ? 50 : 100
}
PropertyChanges {
target: rightButtons
anchors.rightMargin: Constants.isSmallDesktopLayout ? 50 : 100
}
},
State {
name: "mobileLayout"
@ -307,21 +315,11 @@ Pane {
anchors.leftMargin: 14
spacing: 9
}
AnchorChanges {
target: leftButtons
anchors.verticalCenter: thermostat.verticalCenter
anchors.top: undefined
}
PropertyChanges {
target: rightButtons
anchors.rightMargin: 14
spacing: 9
}
AnchorChanges {
target: rightButtons
anchors.verticalCenter: thermostat.verticalCenter
anchors.top: undefined
}
PropertyChanges {
target: powerToggle
visible: true

View File

@ -34,7 +34,7 @@ Pane {
readonly property int contentHeight: root.height - title.height
- root.topPadding - root.bottomPadding
readonly property int contentWidth: root.width - root.rightPadding - root.leftPadding
readonly property bool isOneColumn: contentWidth < 900
readonly property bool isOneColumn: Constants.isSmallLayout || Constants.isMobileLayout
}
Column {

View File

@ -106,8 +106,8 @@ Rectangle {
states: [
State {
name: "desktopLayout"
when: Constants.isBigDesktopLayout || Constants.isSmallDesktopLayout
name: "bigDesktopLayout"
when: Constants.isBigDesktopLayout
PropertyChanges {
target: root
width: 1010
@ -123,6 +123,24 @@ Rectangle {
itemTopMargin: 55
}
},
State {
name: "smallDesktopLayout"
when: Constants.isSmallDesktopLayout
PropertyChanges {
target: root
width: 830
height: 200
}
PropertyChanges {
target: internal
hourSize: 21
itemWidth: 33
itemHeight: 120
columnHeight: 165
bottomHourVisibility: true
itemTopMargin: 44
}
},
State {
name: "mobileLayout"
when: Constants.isMobileLayout

View File

@ -30,19 +30,18 @@
It shows how to implement different designs depending on the window size.
\section1 Responsive Design
As mentioned above, the application has support for a variety of display sizes.It can scale dynamically when the user changes the window size, or the application will select the correct sizes based on the available display on mobile targets.
As mentioned above, the application has support for a variety of display sizes. It can scale dynamically when the user changes the window size, or the application will select the correct sizes based on the available display on mobile targets.
Properties that specify the display size and control which layout is currently in use have been created in \c Constants.qml to achieve this behavior.
\quotefromfile demos/thermostat/imports/Thermostat/Constants.qml
\skipto isBigDesktopLayout
\printuntil isSmallLayout
In \c App.qml, the properties were bound to the window height and width at application startup.
In \c App.qml, the properties were bound to the window height and width at application startup. They are mutually exclusive.
\quotefromfile demos/thermostat/content/App.qml
\skipto Component.onCompleted
\printuntil Constants.isSmallLayout
\printuntil }
\printuntil })
\printuntil }
The states are then used to control the properties of the component such as width, height, fontSize, position, layout (column or row), etc.

View File

@ -5,15 +5,15 @@ pragma Singleton
import QtQuick
QtObject {
readonly property int width: 1440
readonly property int height: 1080
property string currentView: "RoomsView"
property bool isBigDesktopLayout: true
property bool isSmallDesktopLayout: false
property bool isMobileLayout: false
property bool isSmallLayout: false
property bool isBigDesktopLayout: layout === Constants.Layout.Desktop
property bool isSmallDesktopLayout: layout === Constants.Layout.SmallDesktop
property bool isMobileLayout: layout === Constants.Layout.Mobile
property bool isSmallLayout: layout === Constants.Layout.Small
enum Layout { Desktop, SmallDesktop, Mobile, Small }
property int layout: Constants.Layout.Desktop
readonly property font smallTitleFont: Qt.font({
"family": "Inter",

View File

@ -8,9 +8,7 @@ import Thermostat
Pane {
id: root
width: 900
height: 580
anchors.fill: parent
padding: 0
required property list<int> energyValues

View File

@ -136,7 +136,7 @@ Pane {
states: [
State {
name: "desktopLayout"
when: Constants.isBigDesktopLayout || Constants.isSmallDesktopLayout
when: Constants.isSmallDesktopLayout || Constants.isBigDesktopLayout
PropertyChanges {
target: root
width: 520