Heavy changes

This commit is contained in:
Stefano Moretti 2023-04-21 18:07:17 +02:00
parent 32a567a950
commit 11606b8f39
52 changed files with 1578 additions and 748 deletions

View File

@ -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
$<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:
QT_QML_DEBUG
# enable deprecated warnings for qt < 5.13
QT_DEPRECATED_WARNINGS
>
target_compile_definitions(baseui
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
$<$<CONFIG:Release>:
# disable deprecated warnings for qt >= 5.13
QT_NO_DEPRECATED_WARNINGS
>
)
target_compile_options(${PROJECT_NAME}
target_compile_options(baseui
PRIVATE
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:GNU>>:
-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}"
)

View File

View File

@ -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
}

View File

@ -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
)

View File

@ -1,7 +0,0 @@
QT += quick quickcontrols2
include(baseui/baseui.pri)
SOURCES += main.cpp
RESOURCES += qml.qrc

View File

@ -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: "<a href='" + url + "'>" + url + "</a>"
text: "<a href='%1'>%1</a>".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<br>"
+ "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: "<a href='https://material.io/tools/icons/'"
+ "title='Material Design'>Material Design</a>"
+ " 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: {

View File

@ -1,5 +0,0 @@
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>

View File

@ -1,6 +0,0 @@
<RCC>
<qresource prefix="imports/BaseUI/icons">
<file>MaterialIcons-Regular.ttf</file>
<file>codepoints.json</file>
</qresource>
</RCC>

27
qml/App.qml Normal file
View File

@ -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
}

View File

@ -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
}
}

View File

@ -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<Action> 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()
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -1,23 +0,0 @@
<RCC>
<qresource prefix="/imports/BaseUI">
<file>qmldir</file>
<file>App.qml</file>
<file>AppStackPage.qml</file>
<file>AppToolBar.qml</file>
<file>ButtonFlat.qml</file>
<file>ButtonRaised.qml</file>
<file>HorizontalDivider.qml</file>
<file>LabelBody.qml</file>
<file>LabelSubheading.qml</file>
<file>LabelTitle.qml</file>
<file>PopupColorSelection.qml</file>
<file>PopupError.qml</file>
<file>PopupInfo.qml</file>
<file>PopupList.qml</file>
<file>PopupModalBase.qml</file>
<file>PopupToast.qml</file>
<file>SettingsItem.qml</file>
<file>SettingsSectionTitle.qml</file>
<file>Style.qml</file>
</qresource>
</RCC>

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

152
qml/DatePicker.qml Normal file
View File

@ -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"
}
}
}
}
}

46
qml/EdgeEffect.qml Normal file
View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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
}
}

20
qml/Icon.qml Normal file
View File

@ -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"
}
}
}

8
qml/LabelBody.qml Normal file
View File

@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Label {
Layout.fillWidth: true
opacity: Style.opacityBodyAndButton
}

View File

@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Label {
Layout.fillWidth: true
opacity: Style.opacityBodySecondary
}

View File

@ -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

9
qml/LabelTitle.qml Normal file
View File

@ -0,0 +1,9 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Label {
Layout.fillWidth: true
font.pixelSize: Style.fontSizeTitle
opacity: Style.opacityTitle
}

View File

@ -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"
}
}

59
qml/OptionsDialog.qml Normal file
View File

@ -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
}
}
}

View File

@ -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 {

45
qml/PopupInfo.qml Normal file
View File

@ -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
}
}
}

View File

@ -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

31
qml/SettingsCheckItem.qml Normal file
View File

@ -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
}
}
}

36
qml/SettingsItem.qml Normal file
View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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

256
qml/TimeCircle.qml Normal file
View File

@ -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"
}
}
}

206
qml/TimePickerCircular.qml Normal file
View File

@ -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
}
}
}

142
qml/TimePickerTumbler.qml Normal file
View File

@ -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
}
}
}

23
qml/shaders/clock.frag Normal file
View File

@ -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;
}

17
qml/shaders/icon.frag Normal file
View File

@ -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;
}

View File

@ -1,41 +1,23 @@
#include <BaseUI/core.h>
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQuickStyle>
#include <QDebug>
#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<Icons>("BaseUI", 1, 0, "Icons", Icons::singletonProvider);
Icons::instance = std::make_unique<Icons>();
#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");
}
}

View File

@ -9,21 +9,29 @@
#include <QJsonObject>
#include <QPainter>
#include <QFontMetrics>
#include <QVariantMap>
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;
};

View File

@ -1,55 +1,52 @@
#include "icons.h"
#include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QVariant>
#include <QQmlEngine>
#include <QFontDatabase>
#include <QColor>
#include <QVariantHash>
#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);
}
}

View File

@ -1,10 +1,13 @@
#ifndef ICONS_H
#define ICONS_H
#ifndef BASEUI_ICONS_H
#define BASEUI_ICONS_H
#include <QQmlEngine>
#include <QQmlPropertyMap>
class QQmlEngine;
class QJSEngine;
#include <memory>
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<Icons> 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 <typename DerivedType>
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

12
src/plugin.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef BASEUI_PLUGIN_H
#define BASEUI_PLUGIN_H
#include <QtQml/QQmlEngineExtensionPlugin>
class BaseUIPlugin : public QQmlEngineExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid)
};
#endif