From 11606b8f39abfe04fcc6e08ec60fc8a728fd15b3 Mon Sep 17 00:00:00 2001 From: Stefano Moretti Date: Fri, 21 Apr 2023 18:07:17 +0200 Subject: [PATCH] Heavy changes --- CMakeLists.txt | 144 ++++++---- LICENSE => UNLICENSE | 0 baseui.pri | 54 ---- example/CMakeLists.txt | 28 +- example/example.pro | 7 - example/main.qml | 234 ++++++++++------ example/qml.qrc | 5 - icons/baseui_icons.qrc | 6 - qml/App.qml | 27 ++ qml/{BaseUI => }/AppStackPage.qml | 22 +- qml/{BaseUI => }/AppToolBar.qml | 22 +- qml/BaseUI/App.qml | 28 -- qml/BaseUI/LabelBody.qml | 9 - qml/BaseUI/LabelTitle.qml | 10 - qml/BaseUI/PopupColorSelection.qml | 84 ------ qml/BaseUI/PopupInfo.qml | 40 --- qml/BaseUI/PopupList.qml | 32 --- qml/BaseUI/PopupModalBase.qml | 14 - qml/BaseUI/SettingsItem.qml | 48 ---- qml/BaseUI/SettingsSectionTitle.qml | 18 -- qml/BaseUI/baseui_qml.qrc | 23 -- qml/BaseUI/qmldir | 21 -- .../ButtonRaised.qml => ButtonContained.qml} | 25 +- qml/{BaseUI => }/ButtonFlat.qml | 17 +- qml/DatePicker.qml | 152 +++++++++++ qml/EdgeEffect.qml | 46 ++++ qml/{BaseUI => }/HorizontalDivider.qml | 11 +- qml/HorizontalListDivider.qml | 15 + qml/Icon.qml | 20 ++ qml/LabelBody.qml | 8 + qml/LabelBodySecondary.qml | 8 + qml/{BaseUI => }/LabelSubheading.qml | 7 +- qml/LabelTitle.qml | 9 + qml/ListViewEdgeEffect.qml | 42 +++ qml/OptionsDialog.qml | 59 ++++ qml/{BaseUI => }/PopupError.qml | 24 +- qml/PopupInfo.qml | 45 +++ qml/{BaseUI => }/PopupToast.qml | 8 +- qml/SettingsCheckItem.qml | 31 +++ qml/SettingsItem.qml | 36 +++ qml/SettingsSectionTitle.qml | 18 ++ qml/{BaseUI => }/Style.qml | 4 +- qml/TimeCircle.qml | 256 ++++++++++++++++++ qml/TimePickerCircular.qml | 206 ++++++++++++++ qml/TimePickerTumbler.qml | 142 ++++++++++ qml/shaders/clock.frag | 23 ++ qml/shaders/icon.frag | 17 ++ src/core.cpp | 32 +-- src/iconprovider.h | 62 ++--- src/icons.cpp | 65 +++-- src/icons.h | 50 +++- src/plugin.h | 12 + 52 files changed, 1578 insertions(+), 748 deletions(-) rename LICENSE => UNLICENSE (100%) delete mode 100644 baseui.pri delete mode 100644 example/example.pro delete mode 100644 example/qml.qrc delete mode 100644 icons/baseui_icons.qrc create mode 100644 qml/App.qml rename qml/{BaseUI => }/AppStackPage.qml (55%) rename qml/{BaseUI => }/AppToolBar.qml (62%) delete mode 100644 qml/BaseUI/App.qml delete mode 100644 qml/BaseUI/LabelBody.qml delete mode 100644 qml/BaseUI/LabelTitle.qml delete mode 100644 qml/BaseUI/PopupColorSelection.qml delete mode 100644 qml/BaseUI/PopupInfo.qml delete mode 100644 qml/BaseUI/PopupList.qml delete mode 100644 qml/BaseUI/PopupModalBase.qml delete mode 100644 qml/BaseUI/SettingsItem.qml delete mode 100644 qml/BaseUI/SettingsSectionTitle.qml delete mode 100644 qml/BaseUI/baseui_qml.qrc delete mode 100644 qml/BaseUI/qmldir rename qml/{BaseUI/ButtonRaised.qml => ButtonContained.qml} (58%) rename qml/{BaseUI => }/ButtonFlat.qml (65%) create mode 100644 qml/DatePicker.qml create mode 100644 qml/EdgeEffect.qml rename qml/{BaseUI => }/HorizontalDivider.qml (60%) create mode 100644 qml/HorizontalListDivider.qml create mode 100644 qml/Icon.qml create mode 100644 qml/LabelBody.qml create mode 100644 qml/LabelBodySecondary.qml rename qml/{BaseUI => }/LabelSubheading.qml (51%) create mode 100644 qml/LabelTitle.qml create mode 100644 qml/ListViewEdgeEffect.qml create mode 100644 qml/OptionsDialog.qml rename qml/{BaseUI => }/PopupError.qml (78%) create mode 100644 qml/PopupInfo.qml rename qml/{BaseUI => }/PopupToast.qml (88%) create mode 100644 qml/SettingsCheckItem.qml create mode 100644 qml/SettingsItem.qml create mode 100644 qml/SettingsSectionTitle.qml rename qml/{BaseUI => }/Style.qml (97%) create mode 100644 qml/TimeCircle.qml create mode 100644 qml/TimePickerCircular.qml create mode 100644 qml/TimePickerTumbler.qml create mode 100644 qml/shaders/clock.frag create mode 100644 qml/shaders/icon.frag create mode 100644 src/plugin.h diff --git a/CMakeLists.txt b/CMakeLists.txt index be11acb..7efd3aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,78 +1,95 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.19) -project(baseui VERSION 0.1 LANGUAGES CXX) +project(BaseUI VERSION 1.0 LANGUAGES CXX) -set(CMAKE_INCLUDE_CURRENT_DIR ON) +include(GNUInstallDirs) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -option(BASEUI_EMBED_QML "BaseUI embed qml" OFF) -option(BASEUI_EMBED_ICONS "BaseUI embed icons" OFF) +option(BASEUI_INCLUDE_ICONS "Include Material icons" ON) -find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Qml Quick Gui QuickControls2 REQUIRED) +find_package(Qt6 COMPONENTS Core Gui Qml Quick QuickControls2 ShaderTools REQUIRED) -add_library(${PROJECT_NAME} STATIC - include/BaseUI/core.h - src/core.cpp - src/iconprovider.h - src/icons.h - src/icons.cpp +set_source_files_properties(qml/Style.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) + +qt_add_qml_module(baseui + URI BaseUI + VERSION 1.0 + SOURCES + include/BaseUI/core.h + src/core.cpp + src/iconprovider.h + src/icons.cpp + src/icons.h + src/plugin.h + QML_FILES + qml/App.qml + qml/AppStackPage.qml + qml/AppToolBar.qml + qml/ButtonContained.qml + qml/ButtonFlat.qml + qml/DatePicker.qml + qml/EdgeEffect.qml + qml/HorizontalDivider.qml + qml/HorizontalListDivider.qml + qml/Icon.qml + qml/LabelBody.qml + qml/LabelBodySecondary.qml + qml/LabelSubheading.qml + qml/LabelTitle.qml + qml/ListViewEdgeEffect.qml + qml/OptionsDialog.qml + qml/PopupError.qml + qml/PopupInfo.qml + qml/PopupToast.qml + qml/SettingsCheckItem.qml + qml/SettingsItem.qml + qml/SettingsSectionTitle.qml + qml/Style.qml + qml/TimeCircle.qml + qml/TimePickerCircular.qml + qml/TimePickerTumbler.qml + RESOURCE_PREFIX + "/baseui/imports" ) -if(BASEUI_EMBED_QML) - target_sources(${PROJECT_NAME} PRIVATE "qml/BaseUI/baseui_qml.qrc") - target_compile_definitions(${PROJECT_NAME} PRIVATE BASEUI_EMBED_QML) -else() - file(GLOB QML_FILES "qml/BaseUI/*.qml" "qml/BaseUI/qmldir") +qt_add_shaders(baseui "baseui_shaders" + BATCHABLE + PRECOMPILE + PREFIX + "/baseui/imports/BaseUI" + FILES + qml/shaders/clock.frag + qml/shaders/icon.frag +) - add_custom_target(copy_qml_to_binary_dir ALL - COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/BaseUI" - COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${QML_FILES} "${CMAKE_BINARY_DIR}/BaseUI" - COMMENT "Copying QML files to binary directory" - VERBATIM +if(BASEUI_INCLUDE_ICONS) + target_compile_definitions(baseui PRIVATE BASEUI_INCLUDE_ICONS) + qt_add_resources(baseui "baseui_icons" + PREFIX + "/baseui/imports/BaseUI" + FILES + icons/codepoints.json + icons/MaterialIcons-Regular.ttf ) endif() -if(BASEUI_EMBED_ICONS) - target_sources(${PROJECT_NAME} PRIVATE "icons/baseui_icons.qrc") - target_compile_definitions(${PROJECT_NAME} PRIVATE BASEUI_EMBED_ICONS) -else() - add_custom_target(copy_icons_to_binary_dir ALL - COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/BaseUI/icons" - COMMAND "${CMAKE_COMMAND}" -E copy_if_different - "${PROJECT_SOURCE_DIR}/icons/codepoints.json" - "${PROJECT_SOURCE_DIR}/icons/MaterialIcons-Regular.ttf" - "${CMAKE_BINARY_DIR}/BaseUI/icons" - COMMENT "Copying icons files to binary directory" - VERBATIM - ) -endif() +target_include_directories(baseui + PUBLIC "${PROJECT_SOURCE_DIR}/include" + PRIVATE "${PROJECT_SOURCE_DIR}/src" +) -target_include_directories(${PROJECT_NAME} PUBLIC "${PROJECT_SOURCE_DIR}/include") - -set_target_properties(${PROJECT_NAME} PROPERTIES - CXX_STANDARD 11 +set_target_properties(baseui PROPERTIES + CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO ) -target_compile_definitions(${PROJECT_NAME} - PRIVATE - $<$,$>: - QT_QML_DEBUG - # enable deprecated warnings for qt < 5.13 - QT_DEPRECATED_WARNINGS - > +target_compile_definitions(baseui + PRIVATE $<$,$>:QT_QML_DEBUG>) - $<$: - # disable deprecated warnings for qt >= 5.13 - QT_NO_DEPRECATED_WARNINGS - > -) - -target_compile_options(${PROJECT_NAME} +target_compile_options(baseui PRIVATE $<$,$>: -Wall @@ -85,10 +102,17 @@ target_compile_options(${PROJECT_NAME} > ) -target_link_libraries(${PROJECT_NAME} +target_link_libraries(baseui PRIVATE - Qt${QT_VERSION_MAJOR}::Qml - Qt${QT_VERSION_MAJOR}::Quick - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::QuickControls2 + Qt::Core + Qt::Gui + Qt::Qml + Qt::Quick + Qt::QuickControls2 +) + +install(TARGETS baseui + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + BUNDLE DESTINATION "." + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ) diff --git a/LICENSE b/UNLICENSE similarity index 100% rename from LICENSE rename to UNLICENSE diff --git a/baseui.pri b/baseui.pri deleted file mode 100644 index 6af2aac..0000000 --- a/baseui.pri +++ /dev/null @@ -1,54 +0,0 @@ -QT += qml quick quickcontrols2 - -BASEUI_DIR = BaseUI - -INCLUDEPATH += \ - $$PWD/include - -HEADERS += \ - $$PWD/include/BaseUI/core.h \ - $$PWD/src/iconprovider.h \ - $$PWD/src/icons.h - -SOURCES += \ - $$PWD/src/core.cpp \ - $$PWD/src/icons.cpp - -QML_FILES = \ - $$PWD/qml/BaseUI/qmldir \ - $$files($$PWD/qml/BaseUI/*.qml) - -OTHER_FILES += $$QML_FILES - -contains(CONFIG, baseui_embed_qml) { - DEFINES += BASEUI_EMBED_QML - RESOURCES += $$PWD/qml/BaseUI/baseui_qml.qrc -} else { - qml_copy.path = $$BASEUI_DIR - qml_copy.files = $$QML_FILES - - qml_install.path = $$DESTDIR/$$BASEUI_DIR - qml_install.files = $$QML_FILES - - COPIES += qml_copy - INSTALLS += qml_install -} - -contains(CONFIG, baseui_embed_icons) { - DEFINES += BASEUI_EMBED_ICONS - RESOURCES += $$PWD/icons/baseui_icons.qrc -} else { - BASEUI_ICONS_DIR = $$BASEUI_DIR/icons - ICONS_FILES = \ - $$PWD/icons/codepoints.json \ - $$PWD/icons/MaterialIcons-Regular.ttf - - icons_copy.path = $$BASEUI_ICONS_DIR - icons_copy.files = $$ICONS_FILES - - icons_install.path = $$DESTDIR/$$BASEUI_ICONS_DIR - icons_install.files = $$ICONS_FILES - - COPIES += icons_copy - INSTALLS += icons_install -} diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index d4fd682..631e13e 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -2,20 +2,28 @@ cmake_minimum_required(VERSION 3.10) project(example VERSION 0.1 LANGUAGES CXX) -find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Qml Quick Gui QuickControls2 REQUIRED) +find_package(Qt6 COMPONENTS Core Gui Qml Quick QuickControls2 REQUIRED) +set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -add_executable(${PROJECT_NAME} main.cpp qml.qrc) +add_subdirectory(BaseUI) -add_subdirectory(baseui) +qt_add_executable(example WIN32 MACOSX_BUNDLE main.cpp) -target_link_libraries(${PROJECT_NAME} - PRIVATE - Qt${QT_VERSION_MAJOR}::Qml - Qt${QT_VERSION_MAJOR}::Quick - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::QuickControls2 +qt_add_qml_module(example + URI Example + VERSION 1.0 + QML_FILES main.qml + NO_RESOURCE_TARGET_PATH +) + +target_link_libraries(example + PUBLIC + Qt::Core + Qt::Gui + Qt::Qml + Qt::Quick + Qt::QuickControls2 baseui ) diff --git a/example/example.pro b/example/example.pro deleted file mode 100644 index 2d08a24..0000000 --- a/example/example.pro +++ /dev/null @@ -1,7 +0,0 @@ -QT += quick quickcontrols2 - -include(baseui/baseui.pri) - -SOURCES += main.cpp - -RESOURCES += qml.qrc diff --git a/example/main.qml b/example/main.qml index e8f4819..889320b 100644 --- a/example/main.qml +++ b/example/main.qml @@ -1,14 +1,14 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.Material 2.12 -import QtQuick.Layouts 1.12 +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Layouts -import BaseUI 1.0 as UI +import BaseUI as UI UI.App { id: root - width: 360 + width: 640 height: 480 property string primary: Material.primary @@ -45,6 +45,24 @@ UI.App { rightPadding: 10 text: Qt.application.name } + + RowLayout { + UI.ButtonContained { + text: "Time Picker" + buttonColor: UI.Style.primaryColor + onClicked: timePicker.open() + } + + UI.ButtonContained { + text: timePicker.time24h ? "24hr" : "AM/PM" + buttonColor: UI.Style.primaryColor + onClicked: timePicker.time24h = !timePicker.time24h + } + + UI.LabelBody { + text: timePicker.timeString + } + } } } @@ -68,33 +86,31 @@ UI.App { anchors { left: parent.left; right: parent.right } spacing: 0 - Rectangle { - id: topItem - - height: 140 - color: UI.Style.primaryColor + Label { + text: Qt.application.displayName + color: Material.foreground + font.pixelSize: UI.Style.fontSizeHeadline + padding: (homePage.appToolBar.implicitHeight - contentHeight) / 2 + leftPadding: 20 Layout.fillWidth: true - - Text { - text: Qt.application.name - color: UI.Style.textOnPrimary - font.pixelSize: UI.Style.fontSizeHeadline - wrapMode: Text.WordWrap - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - margins: 25 - } - } } + UI.HorizontalListDivider {} + Repeater { id: pageList model: [ - { icon: UI.Icons.settings, text: "Settings", page: settingsPage }, - { icon: UI.Icons.info_outline, text: "About", page: aboutPage } + { + icon: UI.Icons.settings, + text: "Settings", + page: settingsPageComponent + }, + { + icon: UI.Icons.info_outline, + text: "Info", + page: infoPageComponent + } ] delegate: ItemDelegate { @@ -105,7 +121,7 @@ UI.App { // Disable, or a double click will push the page twice. menuColumn.enabled = false navDrawer.close() - pageStack.push(modelData.page) + homePage.stack.push(modelData.page) } } } @@ -139,15 +155,26 @@ UI.App { onTriggered: infoPopup.open() } } + + UI.TimePickerCircular { + id: timePicker + } } Component { - id: settingsPage + id: settingsPageComponent UI.AppStackPage { + id: settingsPage + title: "Settings" padding: 0 + leftButton: Action { + icon.source: UI.Icons.arrow_back + onTriggered: settingsPage.back() + } + Flickable { contentHeight: settingsPane.implicitHeight anchors.fill: parent @@ -166,24 +193,29 @@ UI.App { text: "Display" } - UI.SettingsItem { + UI.SettingsCheckItem { title: "Dark Theme" - check.visible: true - check.checked: root.theme - check.onClicked: root.theme = !root.theme - onClicked: check.clicked() + checkState: root.theme ? Qt.Checked : Qt.Unchecked + onClicked: root.theme = !root.theme + Layout.fillWidth: true } UI.SettingsItem { title: "Primary Color" - subtitle: primaryColorPopup.currentColorName - onClicked: primaryColorPopup.open() + subtitle: colorDialog.getColorName(root.primary) + onClicked: { + colorDialog.selectAccentColor = false + colorDialog.open() + } } UI.SettingsItem { title: "Accent Color" - subtitle: accentColorPopup.currentColorName - onClicked: accentColorPopup.open() + subtitle: colorDialog.getColorName(root.accent) + onClicked: { + colorDialog.selectAccentColor = true + colorDialog.open() + } } } } @@ -192,18 +224,24 @@ UI.App { } Component { - id: aboutPage + id: infoPageComponent UI.AppStackPage { - title: "About" + id: infoPage + title: "Info" padding: 10 + leftButton: Action { + icon.source: UI.Icons.arrow_back + onTriggered: infoPage.back() + } + Flickable { - contentHeight: aboutPane.implicitHeight + contentHeight: infoPane.implicitHeight anchors.fill: parent Pane { - id: aboutPane + id: infoPane anchors.fill: parent @@ -216,43 +254,11 @@ UI.App { } UI.LabelBody { - property string url: "http://github.com/stemoretti/baseui" - - text: "" + url + "" + text: "%1".arg("http://github.com/stemoretti/BaseUI") linkColor: UI.Style.isDarkTheme ? "lightblue" : "blue" onLinkActivated: Qt.openUrlExternally(link) horizontalAlignment: Qt.AlignHCenter } - - UI.HorizontalDivider { } - - UI.LabelSubheading { - text: "This app is based on the following software:" - wrapMode: Text.WordWrap - } - - UI.LabelBody { - text: "Qt 6
" - + "Copyright 2008-2021 The Qt Company Ltd." - + " All rights reserved." - wrapMode: Text.WordWrap - } - - UI.LabelBody { - text: "Qt is under the LGPLv3 license." - wrapMode: Text.WordWrap - } - - UI.HorizontalDivider { } - - UI.LabelBody { - text: "Material Design" - + " icons are under Apache license version 2.0" - wrapMode: Text.WordWrap - linkColor: UI.Style.isDarkTheme ? "lightblue" : "blue" - onLinkActivated: Qt.openUrlExternally(link) - } } } } @@ -275,23 +281,79 @@ UI.App { text: "Information message." } - UI.PopupColorSelection { - id: primaryColorPopup + UI.OptionsDialog { + id: colorDialog - parent: Overlay.overlay + property bool selectAccentColor: false - currentColor: root.primary - onColorSelected: function(c) { root.primary = c } - } + function getColorName(color) { + var filtered = colorDialog.model.filter((c) => { + return Material.color(c.bg) === color + }) + return filtered.length ? filtered[0].name : "" + } - UI.PopupColorSelection { - id: accentColorPopup + title: selectAccentColor ? qsTr("Choose accent color") : qsTr("Choose primary color") + model: [ + { name: "Material Red", bg: Material.Red }, + { name: "Material Pink", bg: Material.Pink }, + { name: "Material Purple", bg: Material.Purple }, + { name: "Material DeepPurple", bg: Material.DeepPurple }, + { name: "Material Indigo", bg: Material.Indigo }, + { name: "Material Blue", bg: Material.Blue }, + { name: "Material LightBlue", bg: Material.LightBlue }, + { name: "Material Cyan", bg: Material.Cyan }, + { name: "Material Teal", bg: Material.Teal }, + { name: "Material Green", bg: Material.Green }, + { name: "Material LightGreen", bg: Material.LightGreen }, + { name: "Material Lime", bg: Material.Lime }, + { name: "Material Yellow", bg: Material.Yellow }, + { name: "Material Amber", bg: Material.Amber }, + { name: "Material Orange", bg: Material.Orange }, + { name: "Material DeepOrange", bg: Material.DeepOrange }, + { name: "Material DeepOrange", bg: Material.DeepOrange }, + { name: "Material Brown", bg: Material.Brown }, + { name: "Material Grey", bg: Material.Grey }, + { name: "Material BlueGrey", bg: Material.BlueGrey } + ] + delegate: RowLayout { + spacing: 0 - parent: Overlay.overlay + Rectangle { + visible: colorDialog.selectAccentColor + color: UI.Style.primaryColor + Layout.margins: 0 + Layout.leftMargin: 10 + Layout.minimumWidth: 48 + Layout.minimumHeight: 32 + } - selectAccentColor: true - currentColor: root.accent - onColorSelected: function(c) { root.accent = c } + Rectangle { + color: Material.color(modelData.bg) + Layout.margins: 0 + Layout.leftMargin: colorDialog.selectAccentColor ? 0 : 10 + Layout.minimumWidth: 32 + Layout.minimumHeight: 32 + } + + RadioButton { + checked: { + if (colorDialog.selectAccentColor) + Material.color(modelData.bg) === root.accent + else + Material.color(modelData.bg) === root.primary + } + text: modelData.name + Layout.leftMargin: 4 + onClicked: { + colorDialog.close() + if (colorDialog.selectAccentColor) + root.accent = Material.color(modelData.bg) + else + root.primary = Material.color(modelData.bg) + } + } + } } Component.onCompleted: { diff --git a/example/qml.qrc b/example/qml.qrc deleted file mode 100644 index 5f6483a..0000000 --- a/example/qml.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - main.qml - - diff --git a/icons/baseui_icons.qrc b/icons/baseui_icons.qrc deleted file mode 100644 index 7b7e4eb..0000000 --- a/icons/baseui_icons.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - MaterialIcons-Regular.ttf - codepoints.json - - diff --git a/qml/App.qml b/qml/App.qml new file mode 100644 index 0000000..f715b60 --- /dev/null +++ b/qml/App.qml @@ -0,0 +1,27 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material + +ApplicationWindow { + id: root + + property alias initialPage: stackView.initialItem + + visible: true + locale: Qt.locale("en_US") + + header: stackView.currentItem?.appToolBar + + StackView { + id: stackView + + anchors.fill: parent + + // make sure that the phone physical back button will get key events + onCurrentItemChanged: stackView.currentItem.forceActiveFocus() + } + + Material.primary: Style.primaryColor + Material.accent: Style.accentColor + Material.theme: Style.isDarkTheme ? Material.Dark : Material.Light +} diff --git a/qml/BaseUI/AppStackPage.qml b/qml/AppStackPage.qml similarity index 55% rename from qml/BaseUI/AppStackPage.qml rename to qml/AppStackPage.qml index 89385b9..a7b5c5b 100644 --- a/qml/BaseUI/AppStackPage.qml +++ b/qml/AppStackPage.qml @@ -1,21 +1,18 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 - -import BaseUI 1.0 +import QtQuick +import QtQuick.Controls Page { id: root - property StackView stack: StackView.view + readonly property StackView stack: StackView.view property alias appToolBar: appToolBar property alias leftButton: appToolBar.leftButton property alias rightButtons: appToolBar.rightButtons function pop(item, operation) { - if (stack.currentItem != root) + if (root.stack.currentItem != root) return false - - return stack.pop(item, operation) + return root.stack.pop(item, operation) } function back() { @@ -23,7 +20,7 @@ Page { } Keys.onBackPressed: function(event) { - if (stack.depth > 1) { + if (root.stack.depth > 1) { event.accepted = true back() } else { @@ -31,15 +28,8 @@ Page { } } - Action { - id: backAction - icon.source: Icons.arrow_back - onTriggered: root.back() - } - AppToolBar { id: appToolBar title: root.title - leftButton: stack && stack.depth > 1 ? backAction : null } } diff --git a/qml/BaseUI/AppToolBar.qml b/qml/AppToolBar.qml similarity index 62% rename from qml/BaseUI/AppToolBar.qml rename to qml/AppToolBar.qml index 61913f8..48332d0 100644 --- a/qml/BaseUI/AppToolBar.qml +++ b/qml/AppToolBar.qml @@ -1,8 +1,10 @@ -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls ToolBar { + id: root + property Action leftButton property list rightButtons @@ -14,12 +16,12 @@ ToolBar { anchors { fill: parent; leftMargin: 4; rightMargin: 4 } ToolButton { - icon.source: leftButton ? leftButton.icon.source : "" + icon.source: root.leftButton?.icon.source ?? "" icon.color: Style.textOnPrimary focusPolicy: Qt.NoFocus opacity: Style.opacityTitle - enabled: leftButton && leftButton.enabled - onClicked: leftButton.trigger() + enabled: root.leftButton && root.leftButton.enabled + onClicked: root.leftButton.trigger() } LabelTitle { id: titleLabel @@ -28,14 +30,14 @@ ToolBar { Layout.fillWidth: true } Repeater { - model: rightButtons.length + model: root.rightButtons.length delegate: ToolButton { - icon.source: rightButtons[index].icon.source + icon.source: root.rightButtons[index].icon.source icon.color: Style.textOnPrimary focusPolicy: Qt.NoFocus opacity: Style.opacityTitle - enabled: rightButtons[index].enabled - onClicked: rightButtons[index].trigger() + enabled: root.rightButtons[index].enabled + onClicked: root.rightButtons[index].trigger() } } } diff --git a/qml/BaseUI/App.qml b/qml/BaseUI/App.qml deleted file mode 100644 index af3919c..0000000 --- a/qml/BaseUI/App.qml +++ /dev/null @@ -1,28 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.Material 2.12 - -ApplicationWindow { - property alias pageStack: stackView - property alias initialPage: stackView.initialItem - - visible: true - locale: Qt.locale("en_US") - - header: stackView.currentItem ? stackView.currentItem.appToolBar : null - - StackView { - id: stackView - - anchors.fill: parent - - onCurrentItemChanged: { - // make sure that the phone physical back button will get key events - currentItem.forceActiveFocus() - } - } - - Material.primary: Style.primaryColor - Material.accent: Style.accentColor - Material.theme: Style.isDarkTheme ? Material.Dark : Material.Light -} diff --git a/qml/BaseUI/LabelBody.qml b/qml/BaseUI/LabelBody.qml deleted file mode 100644 index f35319a..0000000 --- a/qml/BaseUI/LabelBody.qml +++ /dev/null @@ -1,9 +0,0 @@ -// ekke (Ekkehard Gentz) @ekkescorner -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 - -Label { - Layout.fillWidth: true - opacity: Style.opacityBodyAndButton -} diff --git a/qml/BaseUI/LabelTitle.qml b/qml/BaseUI/LabelTitle.qml deleted file mode 100644 index 040e9b3..0000000 --- a/qml/BaseUI/LabelTitle.qml +++ /dev/null @@ -1,10 +0,0 @@ -// ekke (Ekkehard Gentz) @ekkescorner -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 - -Label { - Layout.fillWidth: true - font.pixelSize: Style.fontSizeTitle - opacity: Style.opacityTitle -} diff --git a/qml/BaseUI/PopupColorSelection.qml b/qml/BaseUI/PopupColorSelection.qml deleted file mode 100644 index cd72049..0000000 --- a/qml/BaseUI/PopupColorSelection.qml +++ /dev/null @@ -1,84 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.Material 2.12 -import QtQuick.Layouts 1.12 - -PopupModalBase { - property bool selectAccentColor: false - property color currentColor: Material.primary - property string currentColorName: colorModel.get(currentIndex).title - property int currentIndex: 0 - - signal colorSelected(color c) - - implicitWidth: parent.width * 0.9 - implicitHeight: Math.min(colorsList.contentHeight, parent.height * 0.9) - - ListView { - id: colorsList - anchors.fill: parent - clip: true - delegate: ItemDelegate { - width: colorsList.width - contentItem: RowLayout { - spacing: 0 - Rectangle { - visible: selectAccentColor - color: Material.primary - Layout.margins: 0 - Layout.minimumHeight: 32 - Layout.minimumWidth: 48 - } - Rectangle { - color: Material.color(model.bg) - Layout.margins: 0 - Layout.minimumHeight: 32 - Layout.minimumWidth: 32 - } - LabelBody { - text: model.title - Layout.leftMargin: 6 - Layout.fillWidth: true - } - } - onClicked: { - colorSelected(Material.color(model.bg)) - currentIndex = index - close() - } - } - - model: ListModel { - id: colorModel - ListElement { title: "Material Red"; bg: Material.Red } - ListElement { title: "Material Pink"; bg: Material.Pink } - ListElement { title: "Material Purple"; bg: Material.Purple } - ListElement { title: "Material DeepPurple"; bg: Material.DeepPurple } - ListElement { title: "Material Indigo"; bg: Material.Indigo } - ListElement { title: "Material Blue"; bg: Material.Blue } - ListElement { title: "Material LightBlue"; bg: Material.LightBlue } - ListElement { title: "Material Cyan"; bg: Material.Cyan } - ListElement { title: "Material Teal"; bg: Material.Teal } - ListElement { title: "Material Green"; bg: Material.Green } - ListElement { title: "Material LightGreen"; bg: Material.LightGreen } - ListElement { title: "Material Lime"; bg: Material.Lime } - ListElement { title: "Material Yellow"; bg: Material.Yellow } - ListElement { title: "Material Amber"; bg: Material.Amber } - ListElement { title: "Material Orange"; bg: Material.Orange } - ListElement { title: "Material DeepOrange"; bg: Material.DeepOrange } - ListElement { title: "Material Brown"; bg: Material.Brown } - ListElement { title: "Material Grey"; bg: Material.Grey } - ListElement { title: "Material BlueGrey"; bg: Material.BlueGrey } - } - } - - Component.onCompleted: { - for (var i = 0; i < colorModel.count; ++i) { - var tmp = colorModel.get(i) - if (Material.color(tmp.bg) === currentColor) { - currentIndex = i - return - } - } - } -} diff --git a/qml/BaseUI/PopupInfo.qml b/qml/BaseUI/PopupInfo.qml deleted file mode 100644 index 2ddce96..0000000 --- a/qml/BaseUI/PopupInfo.qml +++ /dev/null @@ -1,40 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 - -PopupModalBase { - id: root - - property alias text: popupLabel.text - - closePolicy: Popup.CloseOnEscape - - ColumnLayout { - spacing: 10 - - width: parent.width - - LabelSubheading { - id: popupLabel - - Layout.fillWidth: true - - topPadding: 20 - leftPadding: 8 - rightPadding: 8 - color: Style.popupTextColor - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - linkColor: Style.isDarkTheme ? "lightblue" : "blue" - onLinkActivated: Qt.openUrlExternally(link) - } - - ButtonFlat { - Layout.alignment: Qt.AlignHCenter - - text: "OK" - textColor: Style.accentColor - onClicked: root.close() - } - } -} diff --git a/qml/BaseUI/PopupList.qml b/qml/BaseUI/PopupList.qml deleted file mode 100644 index a936fe4..0000000 --- a/qml/BaseUI/PopupList.qml +++ /dev/null @@ -1,32 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 - -PopupModalBase { - id: root - - property alias model: internalList.model - property alias currentIndex: internalList.currentIndex - property var delegateFunction - - signal clicked(var data, int index) - - implicitWidth: parent.width * 0.9 - implicitHeight: Math.min(internalList.contentHeight, parent.height * 0.9) - - ListView { - id: internalList - anchors.fill: parent - clip: true - highlightMoveDuration: 0 - delegate: ItemDelegate { - id: internalDelegate - width: parent.width - implicitHeight: 40 - text: delegateFunction(modelData) - onClicked: root.clicked(modelData, index) - } - onCurrentIndexChanged: { - internalList.positionViewAtIndex(currentIndex, ListView.Center) - } - } -} diff --git a/qml/BaseUI/PopupModalBase.qml b/qml/BaseUI/PopupModalBase.qml deleted file mode 100644 index 356d022..0000000 --- a/qml/BaseUI/PopupModalBase.qml +++ /dev/null @@ -1,14 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 - -Popup { - id: root - - modal: true - dim: true - padding: 0 - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - implicitWidth: Math.min(contentWidth, parent.width * 0.9) - implicitHeight: Math.min(contentHeight, parent.height * 0.9) -} diff --git a/qml/BaseUI/SettingsItem.qml b/qml/BaseUI/SettingsItem.qml deleted file mode 100644 index f4af6bc..0000000 --- a/qml/BaseUI/SettingsItem.qml +++ /dev/null @@ -1,48 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 - -ItemDelegate { - id: root - - property alias title: titleLabel.text - property string subtitle - property string subtitlePlaceholder - property alias check: settingSwitch - - Layout.fillWidth: true - - contentItem: RowLayout { - ColumnLayout { - spacing: 2 - - LabelSubheading { - id: titleLabel - - wrapMode: Text.WordWrap - - Layout.fillWidth: true - } - LabelBody { - id: subtitleLabel - - visible: text.length > 0 || root.subtitlePlaceholder.length > 0 - opacity: 0.6 - wrapMode: Text.WordWrap - elide: Text.ElideMiddle - text: root.subtitle.length > 0 ? root.subtitle : root.subtitlePlaceholder - - Layout.fillWidth: true - } - } - - Item { - Layout.fillWidth: true - } - - Switch { - id: settingSwitch - visible: false - } - } -} diff --git a/qml/BaseUI/SettingsSectionTitle.qml b/qml/BaseUI/SettingsSectionTitle.qml deleted file mode 100644 index 805a2e8..0000000 --- a/qml/BaseUI/SettingsSectionTitle.qml +++ /dev/null @@ -1,18 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 - -Label { - leftPadding: 16 - topPadding: 6 - bottomPadding: 6 - font.bold: true - font.pixelSize: Style.fontSizeBodyAndButton - color: Style.isDarkTheme ? "white" : "black" - - background: Rectangle { - color: Style.isDarkTheme ? Qt.darker("gray") : "lightgray" - } - - Layout.fillWidth: true -} diff --git a/qml/BaseUI/baseui_qml.qrc b/qml/BaseUI/baseui_qml.qrc deleted file mode 100644 index 40d865f..0000000 --- a/qml/BaseUI/baseui_qml.qrc +++ /dev/null @@ -1,23 +0,0 @@ - - - qmldir - App.qml - AppStackPage.qml - AppToolBar.qml - ButtonFlat.qml - ButtonRaised.qml - HorizontalDivider.qml - LabelBody.qml - LabelSubheading.qml - LabelTitle.qml - PopupColorSelection.qml - PopupError.qml - PopupInfo.qml - PopupList.qml - PopupModalBase.qml - PopupToast.qml - SettingsItem.qml - SettingsSectionTitle.qml - Style.qml - - diff --git a/qml/BaseUI/qmldir b/qml/BaseUI/qmldir deleted file mode 100644 index fd0703c..0000000 --- a/qml/BaseUI/qmldir +++ /dev/null @@ -1,21 +0,0 @@ -module BaseUI - -App 1.0 App.qml -AppStackPage 1.0 AppStackPage.qml -AppToolBar 1.0 AppToolBar.qml -ButtonFlat 1.0 ButtonFlat.qml -ButtonRaised 1.0 ButtonRaised.qml -HorizontalDivider 1.0 HorizontalDivider.qml -LabelBody 1.0 LabelBody.qml -LabelSubheading 1.0 LabelSubheading.qml -LabelTitle 1.0 LabelTitle.qml -PopupColorSelection 1.0 PopupColorSelection.qml -PopupError 1.0 PopupError.qml -PopupInfo 1.0 PopupInfo.qml -PopupList 1.0 PopupList.qml -PopupModalBase 1.0 PopupModalBase.qml -PopupToast 1.0 PopupToast.qml -SettingsItem 1.0 SettingsItem.qml -SettingsSectionTitle 1.0 SettingsSectionTitle.qml - -singleton Style 1.0 Style.qml diff --git a/qml/BaseUI/ButtonRaised.qml b/qml/ButtonContained.qml similarity index 58% rename from qml/BaseUI/ButtonRaised.qml rename to qml/ButtonContained.qml index bfbefaf..07ef936 100644 --- a/qml/BaseUI/ButtonRaised.qml +++ b/qml/ButtonContained.qml @@ -1,9 +1,9 @@ -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls Button { - id: button + id: root property alias textColor: buttonText.color property alias buttonColor: buttonBackground.color @@ -12,17 +12,16 @@ Button { leftPadding: 6 rightPadding: 6 - Layout.minimumWidth: 80 - contentItem: Text { id: buttonText - text: button.text + text: root.text opacity: enabled ? 1.0 : 0.3 color: Style.textOnPrimary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight font.capitalization: Font.AllUppercase + font.weight: Font.Medium } background: Rectangle { @@ -30,16 +29,6 @@ Button { implicitHeight: 48 color: Style.primaryColor radius: 2 - opacity: button.pressed ? 0.75 : 1.0 - /* - layer.enabled: true - layer.effect: DropShadow { - verticalOffset: 2 - horizontalOffset: 1 - color: dropShadow - samples: button.pressed ? 20 : 10 - spread: 0.5 - } - */ + opacity: root.pressed ? 0.75 : 1.0 } } diff --git a/qml/BaseUI/ButtonFlat.qml b/qml/ButtonFlat.qml similarity index 65% rename from qml/BaseUI/ButtonFlat.qml rename to qml/ButtonFlat.qml index e51810a..83f856d 100644 --- a/qml/BaseUI/ButtonFlat.qml +++ b/qml/ButtonFlat.qml @@ -1,10 +1,9 @@ -// ekke (Ekkehard Gentz) @ekkescorner -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls Button { - id: button + id: root property alias textColor: buttonText.color @@ -12,11 +11,9 @@ Button { leftPadding: 6 rightPadding: 6 - Layout.minimumWidth: 88 - contentItem: Text { id: buttonText - text: button.text + text: root.text opacity: enabled ? 1.0 : 0.3 color: Style.flatButtonTextColor horizontalAlignment: Text.AlignHCenter @@ -29,8 +26,8 @@ Button { background: Rectangle { id: buttonBackground implicitHeight: 48 - color: button.pressed ? buttonText.color : "transparent" + color: root.pressed ? buttonText.color : "transparent" radius: 2 - opacity: button.pressed ? 0.12 : 1.0 + opacity: root.pressed ? 0.12 : 1.0 } } diff --git a/qml/DatePicker.qml b/qml/DatePicker.qml new file mode 100644 index 0000000..5a56730 --- /dev/null +++ b/qml/DatePicker.qml @@ -0,0 +1,152 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.Material + +import BaseUI as UI + +Item { + id: root + + property string locale: "en_US" + property date selectedDate: new Date() + + readonly property int day: selectedDate.getDate() + readonly property int month: selectedDate.getMonth() + readonly property int year: selectedDate.getFullYear() + readonly property string dateString: year + "-" + _zeroPad(month + 1) + "-" + _zeroPad(day) + + function _zeroPad(n) { return n > 9 ? n : '0' + n } + + implicitHeight: column.implicitHeight + implicitWidth: column.implicitWidth + + ColumnLayout { + id: column + + spacing: 0 + anchors.fill: parent + + Rectangle { + implicitHeight: monthRow.implicitHeight + color: Material.primary + + Layout.fillWidth: true + + RowLayout { + id: monthRow + + spacing: 6 + width: parent.width + + ToolButton { + leftPadding: 12 + rightPadding: 12 + icon.source: UI.Icons.keyboard_arrow_left + icon.color: Style.textOnPrimary + onClicked: { + if (monthGrid.month > 0) { + monthGrid.month-- + } else { + monthGrid.month = 11 + monthGrid.year-- + } + } + } + + LabelTitle { + text: Qt.locale(root.locale).monthName(monthGrid.month) + " " + monthGrid.year + elide: Text.ElideRight + color: Style.textOnPrimary + horizontalAlignment: Qt.AlignHCenter + Layout.fillWidth: true + } + + ToolButton { + leftPadding: 12 + rightPadding: 12 + icon.source: UI.Icons.keyboard_arrow_right + icon.color: Style.textOnPrimary + onClicked: { + if (monthGrid.month < 11) { + monthGrid.month++ + } else { + monthGrid.month = 0 + monthGrid.year++ + } + } + } + } + } + + DayOfWeekRow { + id: dayOfWeekRow + + leftPadding: 24 + rightPadding: 24 + Layout.fillWidth: true + font.bold: false + + locale: Qt.locale(root.locale) + + delegate: LabelBodySecondary { + text: model.shortName + font: dayOfWeekRow.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + MonthGrid { + id: monthGrid + + rightPadding: 24 + leftPadding: 24 + Layout.fillWidth: true + + month: root.month + year: root.year + + locale: Qt.locale(root.locale) + + onClicked: function(d) { + // Important: check the month to avoid clicking on days outside where opacity 0 + if (d.getMonth() === monthGrid.month) { + root.selectedDate = d + console.log("tapped on a date ") + } else { + console.log("outside valid month " + d.getMonth()) + } + } + + delegate: Label { + id: dayLabel + + readonly property bool selected: + model.day === root.day + && model.month === root.month + && model.year === root.year + + text: model.day + font.bold: model.today ? true : false + font.pixelSize: Style.fontSizeTitle + opacity: model.month === monthGrid.month ? 1 : 0.3 + color: selected + ? Style.textOnPrimary + : (model.today ? Style.accentColor : Material.foreground) + minimumPointSize: 8 + fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + background: Rectangle { + anchors.centerIn: parent + width: Math.min(parent.height + 2, parent.width + 2) + height: Math.min(parent.height + 2, parent.width + 2) + radius: width / 2 + color: parent.selected ? Style.primaryColor : "transparent" + } + } + } + } +} diff --git a/qml/EdgeEffect.qml b/qml/EdgeEffect.qml new file mode 100644 index 0000000..63c6b94 --- /dev/null +++ b/qml/EdgeEffect.qml @@ -0,0 +1,46 @@ +import QtQuick + +Item { + id: root + + enum Side { + Top, + Bottom + } + + required property int overshoot + required property int maxOvershoot + + property int side: EdgeEffect.Side.Top + property color color: "gray" + + implicitHeight: 30 + + onColorChanged: canvas.requestPaint() + + Canvas { + id: canvas + + anchors.fill: parent + opacity: root.overshoot / root.maxOvershoot + + onPaint: { + if (root.side === EdgeEffect.Side.Top) { + var y1 = 0 + var y2 = height + } else { + var y1 = height + var y2 = 0 + } + var ctx = getContext("2d") + ctx.save() + ctx.reset() + ctx.fillStyle = root.color + ctx.beginPath() + ctx.moveTo(0, y1) + ctx.bezierCurveTo(width / 4, y2, 3 * width / 4, y2, width, y1) + ctx.fill() + ctx.restore() + } + } +} diff --git a/qml/BaseUI/HorizontalDivider.qml b/qml/HorizontalDivider.qml similarity index 60% rename from qml/BaseUI/HorizontalDivider.qml rename to qml/HorizontalDivider.qml index 7978881..f9b7928 100644 --- a/qml/BaseUI/HorizontalDivider.qml +++ b/qml/HorizontalDivider.qml @@ -1,13 +1,10 @@ -// ekke (Ekkehard Gentz) @ekkescorner -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 +import QtQuick +import QtQuick.Layouts + Item { height: 8 Layout.fillWidth: true - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.margins: 6 + // https://www.google.com/design/spec/components/dividers.html#dividers-types-of-dividers Rectangle { anchors.centerIn: parent diff --git a/qml/HorizontalListDivider.qml b/qml/HorizontalListDivider.qml new file mode 100644 index 0000000..1d0fe3e --- /dev/null +++ b/qml/HorizontalListDivider.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Layouts + +// special divider for list elements +// using height 1 ensures that it looks good if highlighted +Item { + height: 1 + Layout.fillWidth: true + Rectangle { + width: parent.width + height: 1 + opacity: Style.dividerOpacity + color: Style.dividerColor + } +} diff --git a/qml/Icon.qml b/qml/Icon.qml new file mode 100644 index 0000000..097135e --- /dev/null +++ b/qml/Icon.qml @@ -0,0 +1,20 @@ +import QtQuick + +Item { + id: root + + property alias icon: internal.source + property color color: undefined + + Image { + id: internal + + anchors.fill: parent + layer.enabled: root.color != undefined + layer.samplerName: "maskSource" + layer.effect: ShaderEffect { + property color color: root.color + fragmentShader: "shaders/icon.frag.qsb" + } + } +} diff --git a/qml/LabelBody.qml b/qml/LabelBody.qml new file mode 100644 index 0000000..ad4621f --- /dev/null +++ b/qml/LabelBody.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Label { + Layout.fillWidth: true + opacity: Style.opacityBodyAndButton +} diff --git a/qml/LabelBodySecondary.qml b/qml/LabelBodySecondary.qml new file mode 100644 index 0000000..cd8eba8 --- /dev/null +++ b/qml/LabelBodySecondary.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Label { + Layout.fillWidth: true + opacity: Style.opacityBodySecondary +} diff --git a/qml/BaseUI/LabelSubheading.qml b/qml/LabelSubheading.qml similarity index 51% rename from qml/BaseUI/LabelSubheading.qml rename to qml/LabelSubheading.qml index f7011f7..41a2ca8 100644 --- a/qml/BaseUI/LabelSubheading.qml +++ b/qml/LabelSubheading.qml @@ -1,7 +1,6 @@ -// ekke (Ekkehard Gentz) @ekkescorner -import QtQuick 2.12 -import QtQuick.Layouts 1.12 -import QtQuick.Controls 2.12 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls Label { Layout.fillWidth: true diff --git a/qml/LabelTitle.qml b/qml/LabelTitle.qml new file mode 100644 index 0000000..b855b44 --- /dev/null +++ b/qml/LabelTitle.qml @@ -0,0 +1,9 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Label { + Layout.fillWidth: true + font.pixelSize: Style.fontSizeTitle + opacity: Style.opacityTitle +} diff --git a/qml/ListViewEdgeEffect.qml b/qml/ListViewEdgeEffect.qml new file mode 100644 index 0000000..d803c39 --- /dev/null +++ b/qml/ListViewEdgeEffect.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls + +ListView { + id: root + + boundsMovement: Flickable.StopAtBounds + boundsBehavior: Flickable.DragOverBounds + + // XXX: disable optimizations + cacheBuffer: height * 1000 + + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 200 } + } + moveDisplaced: Transition { + NumberAnimation { property: "y"; duration: 200 } + } + removeDisplaced: Transition { + NumberAnimation { property: "y"; duration: 200 } + } + + ScrollIndicator.vertical: ScrollIndicator { } + + EdgeEffect { + width: root.width + anchors.top: root.top + side: EdgeEffect.Side.Top + overshoot: root.verticalOvershoot < 0 ? -root.verticalOvershoot : 0 + maxOvershoot: root.height + color: Style.isDarkTheme ? "gray" : "darkgray" + } + + EdgeEffect { + width: root.width + anchors.bottom: root.bottom + side: EdgeEffect.Side.Bottom + overshoot: root.verticalOvershoot > 0 ? root.verticalOvershoot : 0 + maxOvershoot: root.height + color: Style.isDarkTheme ? "gray" : "darkgray" + } +} diff --git a/qml/OptionsDialog.qml b/qml/OptionsDialog.qml new file mode 100644 index 0000000..4809125 --- /dev/null +++ b/qml/OptionsDialog.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Dialog { + id: root + + property alias model: listView.model + property alias delegate: listView.delegate + + parent: Overlay.overlay + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + width: Math.min(Math.max(header.implicitWidth, listView.contentItem.childrenRect.width), parent.width * 0.9) + height: Math.min(implicitHeight + listView.contentHeight, parent.height * 0.9) + + padding: 0 + + modal: true + dim: true + + closePolicy: Popup.CloseOnEscape + + onClosed: listView.positionViewAtBeginning() + + contentItem: ColumnLayout { + spacing: 0 + + HorizontalListDivider { + opacity: listView.contentY - listView.originY > 0 ? 1 : 0 + Behavior on opacity { NumberAnimation {} } + } + + ListViewEdgeEffect { + id: listView + + clip: true + + Layout.fillWidth: true + Layout.fillHeight: true + } + + HorizontalListDivider { + opacity: listView.contentHeight - listView.contentY - listView.height > 0 ? 1 : 0 + Behavior on opacity { NumberAnimation {} } + } + } + + footer: DialogButtonBox { + alignment: Qt.AlignRight + + ButtonFlat { + text: qsTr("Cancel") + textColor: Style.accentColor + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + } +} diff --git a/qml/BaseUI/PopupError.qml b/qml/PopupError.qml similarity index 78% rename from qml/BaseUI/PopupError.qml rename to qml/PopupError.qml index 228ea21..c039172 100644 --- a/qml/BaseUI/PopupError.qml +++ b/qml/PopupError.qml @@ -1,10 +1,10 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.Material 2.12 -import QtQuick.Layouts 1.12 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Window -import BaseUI 1.0 +import BaseUI as UI Popup { id: root @@ -45,13 +45,11 @@ Popup { RowLayout { width: parent.width - Image { - id: alarmIcon - - smooth: true - source: Icons.error + "color=white" - sourceSize.width: 36 - sourceSize.height: 36 + Icon { + width: 36 + height: 36 + icon: UI.Icons.error + color: "white" } Label { diff --git a/qml/PopupInfo.qml b/qml/PopupInfo.qml new file mode 100644 index 0000000..cde6be3 --- /dev/null +++ b/qml/PopupInfo.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Dialog { + id: root + + property alias text: popupLabel.text + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + modal: true + dim: true + + closePolicy: Popup.CloseOnEscape + + contentItem: LabelSubheading { + id: popupLabel + + Layout.fillWidth: true + + topPadding: 20 + leftPadding: 8 + rightPadding: 8 + color: Style.popupTextColor + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + linkColor: Style.isDarkTheme ? "lightblue" : "blue" + onLinkActivated: Qt.openUrlExternally(link) + } + + footer: DialogButtonBox { + alignment: Qt.AlignHCenter + standardButtons: DialogButtonBox.Ok + + onAccepted: root.close() + + ButtonFlat { + text: "OK" + textColor: Style.accentColor + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + } +} diff --git a/qml/BaseUI/PopupToast.qml b/qml/PopupToast.qml similarity index 88% rename from qml/BaseUI/PopupToast.qml rename to qml/PopupToast.qml index b5aefd6..3e55f68 100644 --- a/qml/BaseUI/PopupToast.qml +++ b/qml/PopupToast.qml @@ -1,7 +1,7 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Controls.Material 2.12 +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Window Popup { id: root diff --git a/qml/SettingsCheckItem.qml b/qml/SettingsCheckItem.qml new file mode 100644 index 0000000..af81e32 --- /dev/null +++ b/qml/SettingsCheckItem.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +CheckDelegate { + id: root + + property alias title: root.text + property string subtitle + property string subtitlePlaceholder + + contentItem: ColumnLayout { + spacing: 2 + + LabelSubheading { + rightPadding: root.indicator.width + root.spacing + text: root.text + elide: Text.ElideRight + Layout.fillWidth: true + } + + LabelBody { + rightPadding: root.indicator.width + root.spacing + text: root.subtitle.length > 0 ? root.subtitle : root.subtitlePlaceholder + visible: text.length > 0 || root.subtitlePlaceholder.length > 0 + opacity: 0.6 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } +} diff --git a/qml/SettingsItem.qml b/qml/SettingsItem.qml new file mode 100644 index 0000000..6515840 --- /dev/null +++ b/qml/SettingsItem.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +ItemDelegate { + id: root + + property alias title: titleLabel.text + property string subtitle + property string subtitlePlaceholder + + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 2 + + LabelSubheading { + id: titleLabel + + wrapMode: Text.WordWrap + + Layout.fillWidth: true + } + LabelBody { + id: subtitleLabel + + visible: text.length > 0 || root.subtitlePlaceholder.length > 0 + opacity: 0.6 + wrapMode: Text.WordWrap + elide: Text.ElideMiddle + text: root.subtitle.length > 0 ? root.subtitle : root.subtitlePlaceholder + + Layout.fillWidth: true + } + } +} diff --git a/qml/SettingsSectionTitle.qml b/qml/SettingsSectionTitle.qml new file mode 100644 index 0000000..380f114 --- /dev/null +++ b/qml/SettingsSectionTitle.qml @@ -0,0 +1,18 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Label { + leftPadding: 16 + topPadding: 6 + bottomPadding: 6 + font.bold: true + font.pixelSize: Style.fontSizeBodyAndButton + color: Style.isDarkTheme ? "white" : Qt.lighter("gray", 1.1) + + background: Rectangle { + color: Style.isDarkTheme ? Qt.darker("gray") : Qt.lighter("lightgray", 1.1) + } + + Layout.fillWidth: true +} diff --git a/qml/BaseUI/Style.qml b/qml/Style.qml similarity index 97% rename from qml/BaseUI/Style.qml rename to qml/Style.qml index aa505a4..40c0167 100644 --- a/qml/BaseUI/Style.qml +++ b/qml/Style.qml @@ -1,7 +1,7 @@ pragma Singleton -import QtQuick 2.12 -import QtQuick.Controls.Material 2.12 +import QtQuick +import QtQuick.Controls.Material QtObject { property bool isDarkTheme: false diff --git a/qml/TimeCircle.qml b/qml/TimeCircle.qml new file mode 100644 index 0000000..f565ab2 --- /dev/null +++ b/qml/TimeCircle.qml @@ -0,0 +1,256 @@ +import QtQuick + +Item { + id: root + + required property Item screen + + property int hours: 0 + property int minutes: 0 + readonly property string timeString: _zeroPad(hours) + ":" + _zeroPad(minutes) + readonly property bool isPM: hours >= 12 + + property bool pickMinutes: false + property bool time24h: false + + property color clockColor: "gray" + property color clockHandColor: "blue" + property color labelsColor: "white" + property color labelsSelectedColor: labelsColor + property color labelDotColor: labelsColor + + property real innerRadius: clock.radius * 0.5 + property real outerRadius: clock.radius * 0.8 + + property int labelsSize: 20 + property int clockHandCircleSize: 2 * labelsSize + + function update() { + circle.pos = circle.mapToItem(root.screen, 0, circle.height) + circle.pos.y = Window.height - circle.pos.y + } + + function _zeroPad(n) { return n > 9 ? n : '0' + n } + + function _getSelectedAngle(fullAngle) { + if (root.pickMinutes) + return fullAngle / 60 * root.minutes + else if (root.hours >= 12) + return fullAngle / 12 * (root.hours - 12) + else + return fullAngle / 12 * root.hours + } + + implicitWidth: labelsSize * 12 + implicitHeight: implicitWidth + + onPickMinutesChanged: { + handAnimation.enabled = circleAnimation.enabled = true + disableAnimationTimer.start() + } + + Timer { + id: disableAnimationTimer + interval: 400 + repeat: false + onTriggered: handAnimation.enabled = circleAnimation.enabled = false + } + + Rectangle { + id: clock + + width: Math.min(root.width, root.height) + height: width + radius: width / 2 + color: root.clockColor + + MouseArea { + property bool isHold: false + + function getSectorFromAngle(rad, sectors) { + let index = Math.round(rad / (2 * Math.PI) * sectors) + return index < 0 ? index + sectors : index + } + + function selectTime(mouse, tap) { + let x = mouse.x - width / 2 + let y = -(mouse.y - height / 2) + let angle = Math.atan2(x, y) + if (root.pickMinutes) { + if (tap) + root.minutes = getSectorFromAngle(angle, 12) * 5 + else + root.minutes = getSectorFromAngle(angle, 60) + } else { + let hour = getSectorFromAngle(angle, 12) + if (root.time24h) { + let radius = (root.outerRadius + root.innerRadius) / 2 + if (Qt.vector2d(x, y).length() > radius) { + if (hour == 0) + hour = 12 + } else if (hour != 0) { + hour += 12 + } + } else if (root.isPM) { + hour += 12 + } + root.hours = hour + } + } + + anchors.fill: parent + pressAndHoldInterval: 100 + + onClicked: (mouse) => { selectTime(mouse, true); root.pickMinutes = true } + onPositionChanged: (mouse) => { if (isHold) selectTime(mouse) } + onPressAndHold: (mouse) => { isHold = true; selectTime(mouse) } + onReleased: { if (isHold) { isHold = false; root.pickMinutes = true } } + } + + // clock hand + Rectangle { + id: hand + + x: clock.width / 2 - width / 2 + y: clock.height / 2 - height + width: 2 + height: root.pickMinutes + || !root.time24h + || (root.hours != 0 && root.hours <= 12) + ? root.outerRadius + : root.innerRadius + + transformOrigin: Item.Bottom + rotation: _getSelectedAngle(360) + color: root.clockHandColor + antialiasing: true + Behavior on rotation { + id: handAnimation + enabled: false + NumberAnimation { duration: 400 } + } + } + + // label background + Rectangle { + id: circle + + property point pos: Qt.point(x, y) + property real angle: _getSelectedAngle(2 * Math.PI) + + x: clock.width / 2 + hand.height * Math.sin(angle) - width / 2 + y: clock.height / 2 - hand.height * Math.cos(angle) - height / 2 + width: root.clockHandCircleSize + height: width + radius: width / 2 + color: root.clockHandColor + + onXChanged: pos.x = mapToItem(root.screen, 0, 0).x + // OpenGL origin is bottom left + onYChanged: pos.y = Window.height - mapToItem(root.screen, 0, height).y + + Rectangle { + width: 4 + height: width + radius: width / 2 + anchors.centerIn: parent + visible: root.pickMinutes && root.minutes % 5 + color: root.labelDotColor + } + + Behavior on angle { + id: circleAnimation + enabled: false + NumberAnimation { duration: 400 } + } + } + + // centerpoint + Rectangle { + anchors.centerIn: parent + width: 10 + height: width + radius: width / 2 + color: root.clockHandColor + } + + Repeater { + anchors.centerIn: parent + model: [ 0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 ] + delegate: Text { + required property int modelData + required property int index + property real angle: 2 * Math.PI * index / 12 + x: clock.width / 2 + root.innerRadius * Math.sin(angle) - width / 2 + y: clock.height / 2 - root.innerRadius * Math.cos(angle) - height / 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: root.labelsSize + visible: root.time24h + opacity: root.pickMinutes ? 0 : 1 + color: root.labelsColor + text: modelData + layer.enabled: true + layer.samplerName: "maskSource" + layer.effect: shaderEffect + Behavior on opacity { NumberAnimation { duration: 200 } } + } + } + + Repeater { + anchors.centerIn: parent + model: [ 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] + delegate: Text { + required property int modelData + required property int index + property real angle: 2 * Math.PI * index / 12 + x: clock.width / 2 + root.outerRadius * Math.sin(angle) - width / 2 + y: clock.height / 2 - root.outerRadius * Math.cos(angle) - height / 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: root.labelsSize + opacity: root.pickMinutes ? 0 : 1 + color: root.labelsColor + text: modelData + layer.enabled: true + layer.samplerName: "maskSource" + layer.effect: shaderEffect + Behavior on opacity { NumberAnimation { duration: 200 } } + } + } + + Repeater { + anchors.centerIn: parent + model: 60 + delegate: Text { + required property int modelData + required property int index + property real angle: 2 * Math.PI * index / 60 + x: clock.width / 2 + root.outerRadius * Math.sin(angle) - width / 2 + y: clock.height / 2 - root.outerRadius * Math.cos(angle) - height / 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: root.labelsSize + visible: modelData % 5 == 0 + opacity: root.pickMinutes ? 1 : 0 + color: root.labelsColor + text: _zeroPad(modelData) + layer.enabled: true + layer.samplerName: "maskSource" + layer.effect: shaderEffect + Behavior on opacity { NumberAnimation { duration: 200 } } + } + } + } + + Component { + id: shaderEffect + ShaderEffect { + property point pos: circle.pos + property real radius: root.labelsSize + property color color: root.labelsSelectedColor + property real dpi: Screen.devicePixelRatio + fragmentShader: "shaders/clock.frag.qsb" + } + } +} diff --git a/qml/TimePickerCircular.qml b/qml/TimePickerCircular.qml new file mode 100644 index 0000000..288a55e --- /dev/null +++ b/qml/TimePickerCircular.qml @@ -0,0 +1,206 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.Material + +Dialog { + id: root + + property alias time24h: timePicker.time24h + + readonly property string timeString: timePicker.timeString + ":00" + + function setTime(hour, minute) { + timePicker.hours = hour + timePicker.minutes = minute + } + + readonly property bool _isLandscape: parent.width > parent.height + + function _zeroPad(n) { return n > 9 ? n : '0' + n } + + parent: Overlay.overlay + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + padding: 0 + topPadding: 0 + + modal: true + dim: true + focus: true + + onOpened: timePicker.update() + onClosed: timePicker.pickMinutes = false + + on_IsLandscapeChanged: updateTimer.restart() + + Timer { + id: updateTimer + interval: 1 + onTriggered: timePicker.update() + } + + header: Pane { + bottomPadding: 0 + + LabelSubheading { + text: qsTr("Select time") + font.bold: true + opacity: 1 + } + } + + contentItem: Pane { + topPadding: 0 + bottomPadding: 0 + + GridLayout { + flow: root._isLandscape ? GridLayout.LeftToRight : GridLayout.TopToBottom + + GridLayout { + flow: root._isLandscape ? GridLayout.TopToBottom : GridLayout.LeftToRight + + Layout.alignment: Qt.AlignCenter + + RowLayout { + spacing: 0 + + Layout.alignment: Qt.AlignVCenter + + Label { + color: timePicker.pickMinutes ? Material.foreground : Style.primaryColor + font.pixelSize: Style.fontSizeDisplay3 + text: { + let hours = timePicker.hours + if (!timePicker.time24h) { + if (timePicker.isPM) { + if (timePicker.hours != 12) + hours = timePicker.hours - 12 + } else { + if (timePicker.hours == 0) + hours = 12 + } + } + _zeroPad(hours) + } + background: Rectangle { + color: timePicker.pickMinutes ? Qt.darker(Material.background, 1.1) : Qt.lighter(Style.primaryColor) + radius: 4 + } + MouseArea { + anchors.fill: parent + onClicked: timePicker.pickMinutes = false + } + } + + Label { + color: Material.foreground + font.pixelSize: Style.fontSizeDisplay3 + text: ":" + } + + Label { + color: timePicker.pickMinutes ? Style.primaryColor : Material.foreground + font.pixelSize: Style.fontSizeDisplay3 + text: _zeroPad(timePicker.minutes) + background: Rectangle { + color: timePicker.pickMinutes ? Qt.lighter(Style.primaryColor) : Qt.darker(Material.background, 1.1) + radius: 4 + } + MouseArea { + anchors.fill: parent + onClicked: timePicker.pickMinutes = true + } + } + } + + ColumnLayout { + visible: !timePicker.time24h + + Layout.alignment: Qt.AlignHCenter + + Rectangle { + width: amLabel.width + 4 + height: amLabel.height + 4 + radius: 4 + color: timePicker.isPM ? Material.background : Style.primaryColor + + Label { + id: amLabel + anchors.centerIn: parent + font.pixelSize: Style.fontSizeTitle + color: timePicker.isPM ? Material.foreground : Style.textOnPrimary + text: "AM" + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (timePicker.isPM) + timePicker.hours -= 12 + } + } + } + + Rectangle { + width: pmLabel.width + 4 + height: pmLabel.height + 4 + radius: 4 + color: timePicker.isPM ? Style.primaryColor : Material.background + + Label { + id: pmLabel + anchors.centerIn: parent + font.pixelSize: Style.fontSizeTitle + color: timePicker.isPM ? Style.textOnPrimary : Material.foreground + text: "PM" + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (!timePicker.isPM) + timePicker.hours += 12 + } + } + } + } + } + + TimeCircle { + id: timePicker + + screen: Overlay.overlay + clockColor: Qt.darker(Material.background, 1.1) + clockHandColor: Style.primaryColor + labelsColor: Style.isDarkTheme ? "#FFFFFF" : "#000000" + labelsSelectedColor: Style.textOnPrimary + labelDotColor: Style.textOnPrimary + labelsSize: Style.fontSizeTitle + + Layout.alignment: Qt.AlignCenter + Layout.leftMargin: root._isLandscape ? 30 : 0 + } + } + } + + footer: DialogButtonBox { + alignment: Qt.AlignRight + background: Pane {} + + ButtonFlat { + text: qsTr("Cancel") + textColor: Style.primaryColor + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + + ButtonFlat { + text: qsTr("OK") + textColor: Style.primaryColor + implicitWidth: 80 + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + } +} diff --git a/qml/TimePickerTumbler.qml b/qml/TimePickerTumbler.qml new file mode 100644 index 0000000..7ec4917 --- /dev/null +++ b/qml/TimePickerTumbler.qml @@ -0,0 +1,142 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Dialog { + id: root + + readonly property int hours: { + if (root.timeAMPM) { + if (amPmTumbler.currentIndex === 0) { + if (hoursTumbler.currentIndex === 0) + 12 + else + hoursTumbler.currentIndex + 12 + } else { + if (hoursTumbler.currentIndex === 12) + 0 + else + hoursTumbler.currentIndex + } + } else { + hoursTumbler.currentIndex + } + } + readonly property int minutes: minutesTumbler.currentIndex + readonly property string timeString: _zeroPad(hours) + ":" + _zeroPad(minutes) + ":00" + + property bool timeAMPM: false + + function setTime(hour, minute) { + if (root.timeAMPM) { + if (hour >= 12) { + hour -= 12 + // XXX: doesn't work. why? + // amPmTumbler.positionViewAtIndex(0, Tumbler.Center) + amPmTumbler.currentIndex = 0 + } else { + // amPmTumbler.positionViewAtIndex(1, Tumbler.Center) + amPmTumbler.currentIndex = 1 + } + } + hoursTumbler.positionViewAtIndex(hour, Tumbler.Center) + minutesTumbler.positionViewAtIndex(minute, Tumbler.Center) + } + + function _zeroPad(n) { return n > 9 ? n : '0' + n } + + parent: Overlay.overlay + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + padding: 0 + topPadding: 0 + + modal: true + dim: true + focus: true + + header: Pane { + LabelSubheading { + text: qsTr("Select time") + font.bold: true + opacity: 1 + } + } + + contentItem: Pane { + RowLayout { + id: tumblerRow + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + spacing: 10 + + Tumbler { + id: hoursTumbler + + model: root.timeAMPM ? [ 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] : 24 + delegate: delegateComponent + } + + Label { + text: ":" + font.pixelSize: 40 + } + + Tumbler { + id: minutesTumbler + + model: 60 + delegate: delegateComponent + } + + Tumbler { + id: amPmTumbler + + visible: root.timeAMPM + model: ["PM", "AM"] + delegate: Label { + text: modelData + opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 40 + } + } + } + } + + footer: DialogButtonBox { + alignment: Qt.AlignRight + background: Pane {} + + ButtonFlat { + text: qsTr("Cancel") + textColor: Style.primaryColor + // implicitWidth: 80 + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + + ButtonFlat { + text: qsTr("OK") + textColor: Style.primaryColor + implicitWidth: 80 + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + } + + Component { + id: delegateComponent + + Label { + text: _zeroPad(modelData) + opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 40 + } + } +} diff --git a/qml/shaders/clock.frag b/qml/shaders/clock.frag new file mode 100644 index 0000000..3d4837d --- /dev/null +++ b/qml/shaders/clock.frag @@ -0,0 +1,23 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec2 pos; + float radius; + vec4 color; + float dpi; +} ubuf; + +layout(binding = 2) uniform sampler2D maskSource; + +void main() +{ + if (distance(gl_FragCoord.xy / ubuf.dpi, ubuf.pos + ubuf.radius) < ubuf.radius) + fragColor = ubuf.color * texture(maskSource, qt_TexCoord0).a * ubuf.qt_Opacity; + else + fragColor = texture(maskSource, qt_TexCoord0).rgba * ubuf.qt_Opacity; +} diff --git a/qml/shaders/icon.frag b/qml/shaders/icon.frag new file mode 100644 index 0000000..ecdca5d --- /dev/null +++ b/qml/shaders/icon.frag @@ -0,0 +1,17 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 color; +} ubuf; + +layout(binding = 2) uniform sampler2D maskSource; + +void main() +{ + fragColor = ubuf.color * texture(maskSource, qt_TexCoord0).a * ubuf.qt_Opacity; +} diff --git a/src/core.cpp b/src/core.cpp index 07c1e06..73bf2e5 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,41 +1,23 @@ #include -#include #include #include -#include #include "icons.h" -static void initialize(QQmlEngine *engine) -{ -#ifdef BASEUI_EMBED_QML - Q_INIT_RESOURCE(baseui_qml); - engine->addImportPath(":/imports"); -#endif - - QString path = "/BaseUI/icons/"; - -#ifdef BASEUI_EMBED_ICONS - Q_INIT_RESOURCE(baseui_icons); - path = ":/imports" + path; -#else - path = QCoreApplication::applicationDirPath() + path; -#endif - - Icons::registerIcons(engine, path); -} - namespace BaseUI { void init(QQmlEngine *engine) { QQuickStyle::setStyle("Material"); - - initialize(engine); - - qmlRegisterSingletonType("BaseUI", 1, 0, "Icons", Icons::singletonProvider); + Icons::instance = std::make_unique(); +#ifdef BASEUI_INCLUDE_ICONS + QString path = ":/baseui/imports/BaseUI/icons/"; + BaseUI::Icons::registerIcons(engine, path + "MaterialIcons-Regular.ttf", + "Material Icons", path + "codepoints.json"); +#endif + engine->addImportPath(":/baseui/imports"); } } diff --git a/src/iconprovider.h b/src/iconprovider.h index a0102e0..045c3eb 100644 --- a/src/iconprovider.h +++ b/src/iconprovider.h @@ -9,21 +9,29 @@ #include #include #include +#include class IconProvider : public QQuickImageProvider { public: - explicit IconProvider(const QString &family, const QString &codesPath) + IconProvider(const QString &family, const QVariantMap &codes) + : QQuickImageProvider(QQuickImageProvider::Image) + , codepoints(codes) + , font(family) + { + } + + IconProvider(const QString &family, const QString &codesPath) : QQuickImageProvider(QQuickImageProvider::Image) , font(family) { QFile file(codesPath); if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) { - auto jd = QJsonDocument::fromJson(file.readAll()); - if (!jd.isNull()) - codepoints = jd.object(); - else + QJsonDocument jd = QJsonDocument::fromJson(file.readAll()); + if (jd.isNull()) qWarning() << "Invalid codepoints JSON file" << codesPath; + else + codepoints = jd.object().toVariantMap(); } else { qWarning() << "Cannot open icon codes file" << codesPath; qWarning() << file.errorString(); @@ -44,22 +52,13 @@ public: if (size) *size = QSize(width, height); -#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) - QStringList args = id.split(",", QString::SkipEmptyParts); -#else - QStringList args = id.split(",", Qt::SkipEmptyParts); -#endif - QString iconChar("?"); - if (!args.isEmpty()) { - QString name = args.takeFirst(); - if (codepoints.value(name).isUndefined()) - qWarning() << "Icon name" << name << "not found in" << font.family(); - else - iconChar = codepoints[name].toString(); - } else { + if (id.isEmpty()) qWarning() << "Icon name empty"; - } + else if (codepoints.value(id).isNull()) + qWarning() << "Icon name" << id << "not found in" << font.family(); + else + iconChar = codepoints[id].toString(); font.setPixelSize(width < height ? width : height); @@ -72,29 +71,6 @@ public: image.fill(Qt::transparent); QPainter painter(&image); - - for (const QString &arg : args) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) - QStringList attr = arg.split("=", QString::SkipEmptyParts); -#else - QStringList attr = arg.split("=", Qt::SkipEmptyParts); -#endif - if (attr.isEmpty() || attr.size() > 2) { - qWarning() << "Argument" << arg << "not valid."; - } else if (attr[0] == "color") { - if (attr.size() == 2) - painter.setPen(attr[1]); - else - qWarning() << "Attribute color needs a value"; - } else if (attr[0] == "hflip") { - painter.setTransform(QTransform(-1, 0, 0, 0, 1, 0, width, 0, 1)); - } else if (attr[0] == "vflip") { - painter.setTransform(QTransform(1, 0, 0, 0, -1, 0, 0, height, 1)); - } else { - qWarning() << "Unknown attribute" << attr; - } - } - painter.setFont(font); painter.drawText(QRect(0, 0, width, height), Qt::AlignCenter, iconChar); @@ -104,7 +80,7 @@ public: QStringList keys() { return codepoints.keys(); } private: - QJsonObject codepoints; + QVariantMap codepoints; QFont font; }; diff --git a/src/icons.cpp b/src/icons.cpp index 2fdd248..e08fb38 100644 --- a/src/icons.cpp +++ b/src/icons.cpp @@ -1,55 +1,52 @@ #include "icons.h" #include -#include -#include -#include -#include -#include #include -#include +#include #include "iconprovider.h" +namespace BaseUI +{ + Icons::Icons(QObject *parent) : QQmlPropertyMap(this, parent) { - QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); } -Icons *Icons::instance() -{ - static Icons instance_; - - return &instance_; -} - -QObject *Icons::singletonProvider(QQmlEngine *qmlEngine, QJSEngine *jsEngine) -{ - Q_UNUSED(qmlEngine) - Q_UNUSED(jsEngine) - - return instance(); -} - -void Icons::registerIcons(QQmlEngine *engine, const QString &path) +void Icons::registerIcons(QQmlEngine *engine, const QString &fontPath, + const QString &fontName, const QVariantMap &codes) { QString iconProviderName = "baseui_icons"; - if (QFontDatabase::addApplicationFont(path + "MaterialIcons-Regular.ttf") == -1) - qWarning() << "Failed to load font Material"; - - auto iconProvider = new IconProvider("Material Icons", path + "codepoints.json"); + if (QFontDatabase::addApplicationFont(fontPath) == -1) + qWarning() << "Failed to load font:" << fontPath; + auto iconProvider = new IconProvider(fontName, codes); engine->addImageProvider(iconProviderName, iconProvider); -#if (QT_VERSION < QT_VERSION_CHECK(6, 1, 0)) - for (const QString &key : iconProvider->keys()) - instance()->insert(key, QVariant("image://" + iconProviderName + "/" + key + ",")); -#else QVariantHash hash; for (const QString &key : iconProvider->keys()) - hash.insert(key, QVariant("image://" + iconProviderName + "/" + key + ",")); - instance()->insert(hash); -#endif + hash.insert(key, QVariant("image://" + iconProviderName + "/" + key)); + instance->insert(hash); +} + +void Icons::registerIcons(QQmlEngine *engine, const QString &fontPath, + const QString &fontName, const QString &codesPath) +{ + QString iconProviderName = "baseui_icons"; + + if (QFontDatabase::addApplicationFont(fontPath) == -1) + qWarning() << "Failed to load font:" << fontPath; + + auto iconProvider = new IconProvider(fontName, codesPath); + engine->addImageProvider(iconProviderName, iconProvider); + + QVariantHash hash; + for (const QString &key : iconProvider->keys()) + hash.insert(key, QVariant("image://" + iconProviderName + "/" + key)); + instance->insert(hash); +} + } diff --git a/src/icons.h b/src/icons.h index 28b16fc..5a47af7 100644 --- a/src/icons.h +++ b/src/icons.h @@ -1,10 +1,13 @@ -#ifndef ICONS_H -#define ICONS_H +#ifndef BASEUI_ICONS_H +#define BASEUI_ICONS_H +#include #include -class QQmlEngine; -class QJSEngine; +#include + +namespace BaseUI +{ class Icons : public QQmlPropertyMap { @@ -13,11 +16,13 @@ class Icons : public QQmlPropertyMap public: Icons(QObject *parent = nullptr); - static Icons *instance(); - static QObject *singletonProvider(QQmlEngine *qmlEngine, QJSEngine *jsEngine); + inline static std::unique_ptr instance; - static void registerIcons(QQmlEngine *engine, const QString &path); + static void registerIcons(QQmlEngine *engine, const QString &fontPath, + const QString &fontName, const QVariantMap &codes); + static void registerIcons(QQmlEngine *engine, const QString &fontPath, + const QString &fontName, const QString &codesPath); protected: template explicit Icons(DerivedType *derived, QObject *parent = nullptr) @@ -25,4 +30,35 @@ protected: {} }; +struct IconsForeign +{ + Q_GADGET + QML_FOREIGN(Icons) + QML_SINGLETON + QML_NAMED_ELEMENT(Icons) + +public: + static Icons *create(QQmlEngine *, QJSEngine *engine) + { + // The instance has to exist before it is used. We cannot replace it. + Q_ASSERT(Icons::instance); + + // The engine has to have the same thread affinity as the singleton. + Q_ASSERT(engine->thread() == Icons::instance->thread()); + + // There can only be one engine accessing the singleton. + if (s_engine) + Q_ASSERT(engine == s_engine); + else + s_engine = engine; + + return Icons::instance.get(); + } + +private: + inline static QJSEngine *s_engine = nullptr; +}; + +} + #endif diff --git a/src/plugin.h b/src/plugin.h new file mode 100644 index 0000000..39a8586 --- /dev/null +++ b/src/plugin.h @@ -0,0 +1,12 @@ +#ifndef BASEUI_PLUGIN_H +#define BASEUI_PLUGIN_H + +#include + +class BaseUIPlugin : public QQmlEngineExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) +}; + +#endif