diff --git a/resources.qrc b/resources.qrc index 661550384bf7f..768ef496a5903 100644 --- a/resources.qrc +++ b/resources.qrc @@ -3,6 +3,9 @@ src/gui/UserStatusSelector.qml src/gui/UserStatusSelectorDialog.qml src/gui/EmojiPicker.qml + src/gui/UserStatusSelectorButton.qml + src/gui/PredefinedStatusButton.qml + src/gui/BasicComboBox.qml src/gui/ErrorBox.qml src/gui/tray/Window.qml src/gui/tray/UserLine.qml diff --git a/src/gui/BasicComboBox.qml b/src/gui/BasicComboBox.qml new file mode 100644 index 0000000000000..7e59f12518d45 --- /dev/null +++ b/src/gui/BasicComboBox.qml @@ -0,0 +1,99 @@ +/* + * Copyright (C) by Claudio Cambra + * + * This program 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 2 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. + */ + +import QtQuick 2.6 +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.0 + +import Style 1.0 + +ComboBox { + id: clearComboBox + + padding: Style.standardSpacing + + background: Rectangle { + radius: Style.slightlyRoundedButtonRadius + color: Style.buttonBackgroundColor + opacity: clearComboBox.hovered ? Style.hoverOpacity : 1.0 + } + + contentItem: Label { + leftPadding: 0 + rightPadding: clearComboBox.indicator.width + clearComboBox.spacing + + text: clearComboBox.displayText + color: Style.ncTextColor + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: ColorOverlay { + x: clearComboBox.width - clearComboBox.rightPadding + y: clearComboBox.topPadding + (clearComboBox.availableHeight - height) / 2 + cached: true + color: Style.ncTextColor + width: source.width + height: source.height + source: Image { + horizontalAlignment: Qt.AlignRight + verticalAlignment: Qt.AlignVCenter + source: "qrc:///client/theme/white/caret-down.svg" + sourceSize.width: Style.accountDropDownCaretSize + sourceSize.height: Style.accountDropDownCaretSize + Accessible.role: Accessible.PopupMenu + Accessible.name: qsTr("Clear status message menu") + } + } + + popup: Popup { + y: clearComboBox.height - Style.normalBorderWidth + width: clearComboBox.width + implicitHeight: contentItem.implicitHeight + padding: Style.normalBorderWidth + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: clearComboBox.popup.visible ? clearComboBox.delegateModel : null + currentIndex: clearComboBox.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + + background: Rectangle { + color: Style.backgroundColor + border.color: Style.menuBorder + radius: Style.slightlyRoundedButtonRadius + } + } + + + delegate: ItemDelegate { + id: clearStatusDelegate + width: clearComboBox.width + contentItem: Label { + text: modelData + color: Style.ncTextColor + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + highlighted: clearComboBox.highlightedIndex === index + background: Rectangle { + color: clearStatusDelegate.highlighted || clearStatusDelegate.hovered ? Style.lightHover : Style.backgroundColor + } + } +} diff --git a/src/gui/EmojiPicker.qml b/src/gui/EmojiPicker.qml index 6a66dbdcb190a..ef1e367a593b0 100644 --- a/src/gui/EmojiPicker.qml +++ b/src/gui/EmojiPicker.qml @@ -16,6 +16,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Style 1.0 import com.nextcloud.desktopclient 1.0 as NC ColumnLayout { @@ -34,6 +35,7 @@ ColumnLayout { ListView { id: headerLayout Layout.fillWidth: true + Layout.margins: 1 implicitWidth: contentItem.childrenRect.width implicitHeight: metrics.height * 2 @@ -42,24 +44,32 @@ ColumnLayout { model: emojiModel.emojiCategoriesModel delegate: ItemDelegate { + id: headerDelegate width: metrics.height * 2 height: headerLayout.height + background: Rectangle { + color: Style.lightHover + visible: ListView.isCurrentItem || headerDelegate.highlighted || headerDelegate.checked || headerDelegate.down || headerDelegate.hovered + radius: Style.slightlyRoundedButtonRadius + } + contentItem: Text { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: emoji + color: Style.ncTextColor } Rectangle { anchors.bottom: parent.bottom width: parent.width - height: 2 + height: Style.thickBorderWidth visible: ListView.isCurrentItem - color: "grey" + color: Style.menuBorder } @@ -71,15 +81,16 @@ ColumnLayout { } Rectangle { - height: 1 + height: Style.normalBorderWidth Layout.fillWidth: true - color: "grey" + color: Style.menuBorder } GridView { Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: metrics.height * 8 + Layout.margins: Style.normalBorderWidth cellWidth: metrics.height * 2 cellHeight: metrics.height * 2 @@ -90,13 +101,22 @@ ColumnLayout { model: emojiModel.model delegate: ItemDelegate { + id: emojiDelegate width: metrics.height * 2 height: metrics.height * 2 + background: Rectangle { + color: Style.lightHover + visible: ListView.isCurrentItem || emojiDelegate.highlighted || emojiDelegate.checked || emojiDelegate.down || emojiDelegate.hovered + radius: Style.slightlyRoundedButtonRadius + } + contentItem: Text { - anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter text: modelData === undefined ? "" : modelData.unicode + color: Style.ncTextColor } onClicked: { diff --git a/src/gui/PredefinedStatusButton.qml b/src/gui/PredefinedStatusButton.qml new file mode 100644 index 0000000000000..7c0861e41b592 --- /dev/null +++ b/src/gui/PredefinedStatusButton.qml @@ -0,0 +1,37 @@ +/* + * Copyright (C) by Claudio Cambra + * + * This program 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 2 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. + */ + +import QtQuick 2.6 +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import Style 1.0 + +AbstractButton { + id: root + + hoverEnabled: true + padding: Style.standardSpacing + + background: Rectangle { + color: root.hovered || root.checked ? Style.lightHover : "transparent" + radius: Style.slightlyRoundedButtonRadius + } + + contentItem: Label { + text: root.text + color: Style.ncTextColor + } +} diff --git a/src/gui/UserStatusSelector.qml b/src/gui/UserStatusSelector.qml index 5c0b7d86fe37b..1b14849ef3f46 100644 --- a/src/gui/UserStatusSelector.qml +++ b/src/gui/UserStatusSelector.qml @@ -19,103 +19,182 @@ import QtQuick.Controls 2.15 import QtQuick.Window 2.15 import com.nextcloud.desktopclient 1.0 as NC +import Style 1.0 + ColumnLayout { id: rootLayout spacing: 0 property NC.UserStatusSelectorModel userStatusSelectorModel Label { - Layout.topMargin: 16 - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 + Layout.topMargin: Style.standardSpacing * 2 + Layout.leftMargin: Style.standardSpacing + Layout.rightMargin: Style.standardSpacing + Layout.bottomMargin: Style.standardSpacing Layout.alignment: Qt.AlignTop | Qt.AlignHCenter font.bold: true text: qsTr("Online status") + color: Style.ncTextColor } GridLayout { - Layout.margins: 8 + id: topButtonsLayout + + Layout.margins: Style.standardSpacing Layout.alignment: Qt.AlignTop columns: 2 rows: 2 - columnSpacing: 8 - rowSpacing: 8 + columnSpacing: Style.standardSpacing + rowSpacing: Style.standardSpacing - Button { - Layout.fillWidth: true + property int maxButtonHeight: 0 + function updateMaxButtonHeight(newHeight) { + maxButtonHeight = Math.max(maxButtonHeight, newHeight) + } + + UserStatusSelectorButton { checked: NC.UserStatus.Online == userStatusSelectorModel.onlineStatus checkable: true icon.source: userStatusSelectorModel.onlineIcon icon.color: "transparent" text: qsTr("Online") onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.Online) - implicitWidth: 100 - } - Button { + Layout.fillWidth: true + implicitWidth: 200 // Pretty much a hack to ensure all the buttons are equal in width + Layout.preferredHeight: topButtonsLayout.maxButtonHeight + onImplicitHeightChanged: topButtonsLayout.updateMaxButtonHeight(implicitHeight) + Component.onCompleted: topButtonsLayout.updateMaxButtonHeight(implicitHeight) + } + UserStatusSelectorButton { checked: NC.UserStatus.Away == userStatusSelectorModel.onlineStatus checkable: true icon.source: userStatusSelectorModel.awayIcon icon.color: "transparent" text: qsTr("Away") onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.Away) - implicitWidth: 100 + + Layout.fillWidth: true + implicitWidth: 200 + Layout.preferredHeight: topButtonsLayout.maxButtonHeight + onImplicitHeightChanged: topButtonsLayout.updateMaxButtonHeight(implicitHeight) + Component.onCompleted: topButtonsLayout.updateMaxButtonHeight(implicitHeight) } - Button { - Layout.fillWidth: true + UserStatusSelectorButton { checked: NC.UserStatus.DoNotDisturb == userStatusSelectorModel.onlineStatus checkable: true icon.source: userStatusSelectorModel.dndIcon icon.color: "transparent" text: qsTr("Do not disturb") + secondaryText: qsTr("Mute all notifications") onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.DoNotDisturb) - implicitWidth: 100 - } - Button { + Layout.fillWidth: true + implicitWidth: 200 + Layout.preferredHeight: topButtonsLayout.maxButtonHeight + onImplicitHeightChanged: topButtonsLayout.updateMaxButtonHeight(implicitHeight) + Component.onCompleted: topButtonsLayout.updateMaxButtonHeight(implicitHeight) + } + UserStatusSelectorButton { checked: NC.UserStatus.Invisible == userStatusSelectorModel.onlineStatus checkable: true icon.source: userStatusSelectorModel.invisibleIcon icon.color: "transparent" text: qsTr("Invisible") + secondaryText: qsTr("Appear offline") onClicked: userStatusSelectorModel.setOnlineStatus(NC.UserStatus.Invisible) - implicitWidth: 100 + + Layout.fillWidth: true + implicitWidth: 200 + Layout.preferredHeight: topButtonsLayout.maxButtonHeight + onImplicitHeightChanged: topButtonsLayout.updateMaxButtonHeight(implicitHeight) + Component.onCompleted: topButtonsLayout.updateMaxButtonHeight(implicitHeight) } } Label { - Layout.topMargin: 16 - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 + Layout.topMargin: Style.standardSpacing * 2 + Layout.leftMargin: Style.standardSpacing + Layout.rightMargin: Style.standardSpacing + Layout.bottomMargin: Style.standardSpacing Layout.alignment: Qt.AlignTop | Qt.AlignHCenter font.bold: true text: qsTr("Status message") + color: Style.ncTextColor } RowLayout { - Layout.topMargin: 8 - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.bottomMargin: 16 + Layout.topMargin: Style.standardSpacing + Layout.leftMargin: Style.standardSpacing + Layout.rightMargin: Style.standardSpacing + Layout.bottomMargin: Style.standardSpacing * 2 Layout.alignment: Qt.AlignTop Layout.fillWidth: true - Button { + spacing: 0 + + UserStatusSelectorButton { + id: fieldButton + Layout.preferredWidth: userStatusMessageTextField.height Layout.preferredHeight: userStatusMessageTextField.height + text: userStatusSelectorModel.userStatusEmoji + onClicked: emojiDialog.open() + onHeightChanged: topButtonsLayout.maxButtonHeight = Math.max(topButtonsLayout.maxButtonHeight, height) + + primary: true + padding: 0 + z: hovered ? 2 : 0 // Make sure highlight is seen on top of text field + + property color borderColor: showBorder ? Style.ncBlue : Style.menuBorder + + // We create the square with only the top-left and bottom-left rounded corners + // by overlaying different rectangles on top of each other + background: Rectangle { + radius: Style.slightlyRoundedButtonRadius + color: Style.buttonBackgroundColor + border.color: fieldButton.borderColor + border.width: Style.normalBorderWidth + + Rectangle { + anchors.fill: parent + anchors.leftMargin: parent.width / 2 + anchors.rightMargin: -1 + z: 1 + color: Style.buttonBackgroundColor + border.color: fieldButton.borderColor + border.width: Style.normalBorderWidth + } + + Rectangle { // We need to cover the blue border of the non-radiused rectangle + anchors.fill: parent + anchors.leftMargin: parent.width / 4 + anchors.rightMargin: parent.width / 4 + anchors.topMargin: Style.normalBorderWidth + anchors.bottomMargin: Style.normalBorderWidth + z: 2 + color: Style.buttonBackgroundColor + } + } } Popup { id: emojiDialog padding: 0 margins: 0 + clip: true anchors.centerIn: Overlay.overlay + + background: Rectangle { + color: Style.backgroundColor + border.width: Style.normalBorderWidth + border.color: Style.menuBorder + radius: Style.slightlyRoundedButtonRadius + } EmojiPicker { id: emojiPicker @@ -131,37 +210,72 @@ ColumnLayout { id: userStatusMessageTextField Layout.fillWidth: true placeholderText: qsTr("What is your status?") + placeholderTextColor: Style.ncSecondaryTextColor text: userStatusSelectorModel.userStatusMessage + color: Style.ncTextColor selectByMouse: true onEditingFinished: userStatusSelectorModel.setUserStatusMessage(text) + + property color borderColor: activeFocus ? Style.ncBlue : Style.menuBorder + + background: Rectangle { + radius: Style.slightlyRoundedButtonRadius + color: Style.backgroundColor + border.color: userStatusMessageTextField.borderColor + border.width: Style.normalBorderWidth + + Rectangle { + anchors.fill: parent + anchors.rightMargin: parent.width / 2 + z: 1 + color: Style.backgroundColor + border.color: userStatusMessageTextField.borderColor + border.width: Style.normalBorderWidth + } + + Rectangle { // We need to cover the blue border of the non-radiused rectangle + anchors.fill: parent + anchors.leftMargin: parent.width / 4 + anchors.rightMargin: parent.width / 4 + anchors.topMargin: Style.normalBorderWidth + anchors.bottomMargin: Style.normalBorderWidth + z: 2 + color: Style.backgroundColor + } + } } } Repeater { model: userStatusSelectorModel.predefinedStatusesCount - Button { + PredefinedStatusButton { id: control Layout.fillWidth: true - flat: !hovered - hoverEnabled: true + Layout.leftMargin: Style.standardSpacing + Layout.rightMargin: Style.standardSpacing + text: userStatusSelectorModel.predefinedStatus(index).icon + " " + userStatusSelectorModel.predefinedStatus(index).message + " - " + userStatusSelectorModel.predefinedStatusClearAt(index) onClicked: userStatusSelectorModel.setPredefinedStatus(index) } } RowLayout { - Layout.topMargin: 16 - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 + Layout.topMargin: Style.standardSpacing * 2 + Layout.leftMargin: Style.standardSpacing + Layout.rightMargin: Style.standardSpacing + Layout.bottomMargin: Style.standardSpacing Layout.alignment: Qt.AlignTop + spacing: Style.standardSpacing Label { text: qsTr("Clear status message after") + color: Style.ncTextColor } - ComboBox { + BasicComboBox { + id: clearComboBox + Layout.fillWidth: true model: userStatusSelectorModel.clearAtValues displayText: userStatusSelectorModel.clearAt @@ -170,16 +284,18 @@ ColumnLayout { } RowLayout { - Layout.margins: 8 + Layout.margins: Style.standardSpacing Layout.alignment: Qt.AlignTop - Button { + UserStatusSelectorButton { Layout.fillWidth: true + primary: true text: qsTr("Clear status message") onClicked: userStatusSelectorModel.clearUserStatus() } - Button { - highlighted: true + UserStatusSelectorButton { + primary: true + colored: true Layout.fillWidth: true text: qsTr("Set status message") onClicked: userStatusSelectorModel.setUserStatus() @@ -187,7 +303,7 @@ ColumnLayout { } ErrorBox { - Layout.margins: 8 + Layout.margins: Style.standardSpacing Layout.fillWidth: true visible: userStatusSelectorModel.errorMessage != "" diff --git a/src/gui/UserStatusSelectorButton.qml b/src/gui/UserStatusSelectorButton.qml new file mode 100644 index 0000000000000..e67fc3454c80a --- /dev/null +++ b/src/gui/UserStatusSelectorButton.qml @@ -0,0 +1,91 @@ +/* + * Copyright (C) by Claudio Cambra + * + * This program 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 2 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. + */ + +import QtQuick 2.6 +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import Style 1.0 + +AbstractButton { + id: root + + property string secondaryText: "" + property bool colored: false + property bool primary: false + property bool highlighted: false + readonly property bool showBorder: hovered || highlighted || checked + + hoverEnabled: true + padding: Style.standardSpacing + + background: Rectangle { + radius: root.primary ? Style.veryRoundedButtonRadius : Style.mediumRoundedButtonRadius + color: root.colored ? Style.ncBlue : Style.buttonBackgroundColor + opacity: !root.colored && root.primary && !root.hovered ? 0.0 : root.colored && root.hovered ? Style.hoverOpacity : 1.0 + border.color: Style.ncBlue + border.width: root.showBorder && !root.primary ? Style.thickBorderWidth : 0 + } + + contentItem: GridLayout { + columns: 2 + rows: 2 + columnSpacing: Style.standardSpacing + rowSpacing: Style.standardSpacing + + Image { + Layout.column: 0 + Layout.columnSpan: root.text === "" && root.secondaryText == "" ? 2 : 1 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + + source: root.icon.source + visible: root.icon.source !== "" + } + + Label { + Layout.column: root.icon.source === "" ? 0 : 1 + Layout.columnSpan: root.icon.source === "" ? 2 : 1 + Layout.row: 0 + Layout.rowSpan: root.secondaryText === "" ? 2 : 1 + Layout.fillWidth: true + horizontalAlignment: root.primary ? Text.AlignHCenter : Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + text: root.text + wrapMode: Text.Wrap + color: root.colored ? Style.ncHeaderTextColor : Style.ncTextColor + font.bold: root.primary + } + + Label { + Layout.column: root.icon.source === "" ? 0 : 1 + Layout.columnSpan: root.icon.source === "" ? 2 : 1 + Layout.row: 1 + Layout.fillWidth: true + horizontalAlignment: root.primary ? Text.AlignHCenter : Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + text: root.secondaryText + wrapMode: Text.Wrap + color: Style.ncSecondaryTextColor + visible: root.secondaryText !== "" + } + } +} diff --git a/src/gui/UserStatusSelectorDialog.qml b/src/gui/UserStatusSelectorDialog.qml index 3f86f8cf9bd25..9121909ced42e 100644 --- a/src/gui/UserStatusSelectorDialog.qml +++ b/src/gui/UserStatusSelectorDialog.qml @@ -1,9 +1,13 @@ import QtQuick.Window 2.15 +import Style 1.0 import com.nextcloud.desktopclient 1.0 as NC Window { id: dialog + + title: qsTr("Set user status") + color: Style.backgroundColor property NC.UserStatusSelectorModel model: NC.UserStatusSelectorModel { onFinished: dialog.close() diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index 0aaaf5539e19b..d5c7bd35af052 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -15,6 +15,7 @@ QtObject { readonly property color lightHover: Theme.darkMode ? Qt.lighter(backgroundColor, 2) : Qt.darker(backgroundColor, 1.05) readonly property color menuBorder: ncSecondaryTextColor readonly property color backgroundColor: Theme.systemPalette.base + readonly property color buttonBackgroundColor: Theme.systemPalette.button // ErrorBox colors readonly property color errorBoxTextColor: Theme.errorBoxTextColor @@ -48,6 +49,13 @@ QtObject { property int currentAccountButtonRadius: 2 property int currentAccountLabelWidth: 128 + property int normalBorderWidth: 1 + property int thickBorderWidth: 2 + property int veryRoundedButtonRadius: 100 + property int mediumRoundedButtonRadius: 8 + property int slightlyRoundedButtonRadius: 5 + property double hoverOpacity: 0.7 + property url stateOnlineImageSource: Theme.stateOnlineImageSource property url stateOfflineImageSource: Theme.stateOfflineImageSource