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_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
option(BASEUI_EMBED_QML "BaseUI embed qml" OFF) option(BASEUI_INCLUDE_ICONS "Include Material icons" ON)
option(BASEUI_EMBED_ICONS "BaseUI embed icons" OFF)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) find_package(Qt6 COMPONENTS Core Gui Qml Quick QuickControls2 ShaderTools REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Qml Quick Gui QuickControls2 REQUIRED)
add_library(${PROJECT_NAME} STATIC set_source_files_properties(qml/Style.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
include/BaseUI/core.h
src/core.cpp qt_add_qml_module(baseui
src/iconprovider.h URI BaseUI
src/icons.h VERSION 1.0
src/icons.cpp 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) qt_add_shaders(baseui "baseui_shaders"
target_sources(${PROJECT_NAME} PRIVATE "qml/BaseUI/baseui_qml.qrc") BATCHABLE
target_compile_definitions(${PROJECT_NAME} PRIVATE BASEUI_EMBED_QML) PRECOMPILE
else() PREFIX
file(GLOB QML_FILES "qml/BaseUI/*.qml" "qml/BaseUI/qmldir") "/baseui/imports/BaseUI"
FILES
qml/shaders/clock.frag
qml/shaders/icon.frag
)
add_custom_target(copy_qml_to_binary_dir ALL if(BASEUI_INCLUDE_ICONS)
COMMAND "${CMAKE_COMMAND}" -E make_directory "${CMAKE_BINARY_DIR}/BaseUI" target_compile_definitions(baseui PRIVATE BASEUI_INCLUDE_ICONS)
COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${QML_FILES} "${CMAKE_BINARY_DIR}/BaseUI" qt_add_resources(baseui "baseui_icons"
COMMENT "Copying QML files to binary directory" PREFIX
VERBATIM "/baseui/imports/BaseUI"
FILES
icons/codepoints.json
icons/MaterialIcons-Regular.ttf
) )
endif() endif()
if(BASEUI_EMBED_ICONS) target_include_directories(baseui
target_sources(${PROJECT_NAME} PRIVATE "icons/baseui_icons.qrc") PUBLIC "${PROJECT_SOURCE_DIR}/include"
target_compile_definitions(${PROJECT_NAME} PRIVATE BASEUI_EMBED_ICONS) PRIVATE "${PROJECT_SOURCE_DIR}/src"
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(${PROJECT_NAME} PUBLIC "${PROJECT_SOURCE_DIR}/include") set_target_properties(baseui PROPERTIES
CXX_STANDARD 17
set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED YES CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO CXX_EXTENSIONS NO
) )
target_compile_definitions(${PROJECT_NAME} target_compile_definitions(baseui
PRIVATE PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
$<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:
QT_QML_DEBUG
# enable deprecated warnings for qt < 5.13
QT_DEPRECATED_WARNINGS
>
$<$<CONFIG:Release>: target_compile_options(baseui
# disable deprecated warnings for qt >= 5.13
QT_NO_DEPRECATED_WARNINGS
>
)
target_compile_options(${PROJECT_NAME}
PRIVATE PRIVATE
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:GNU>>: $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:GNU>>:
-Wall -Wall
@ -85,10 +102,17 @@ target_compile_options(${PROJECT_NAME}
> >
) )
target_link_libraries(${PROJECT_NAME} target_link_libraries(baseui
PRIVATE PRIVATE
Qt${QT_VERSION_MAJOR}::Qml Qt::Core
Qt${QT_VERSION_MAJOR}::Quick Qt::Gui
Qt${QT_VERSION_MAJOR}::Gui Qt::Qml
Qt${QT_VERSION_MAJOR}::QuickControls2 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) project(example VERSION 0.1 LANGUAGES CXX)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) find_package(Qt6 COMPONENTS Core Gui Qml Quick QuickControls2 REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Qml Quick Gui QuickControls2 REQUIRED)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC 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} qt_add_qml_module(example
PRIVATE URI Example
Qt${QT_VERSION_MAJOR}::Qml VERSION 1.0
Qt${QT_VERSION_MAJOR}::Quick QML_FILES main.qml
Qt${QT_VERSION_MAJOR}::Gui NO_RESOURCE_TARGET_PATH
Qt${QT_VERSION_MAJOR}::QuickControls2 )
target_link_libraries(example
PUBLIC
Qt::Core
Qt::Gui
Qt::Qml
Qt::Quick
Qt::QuickControls2
baseui 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
import QtQuick.Controls 2.12 import QtQuick.Controls
import QtQuick.Controls.Material 2.12 import QtQuick.Controls.Material
import QtQuick.Layouts 1.12 import QtQuick.Layouts
import BaseUI 1.0 as UI import BaseUI as UI
UI.App { UI.App {
id: root id: root
width: 360 width: 640
height: 480 height: 480
property string primary: Material.primary property string primary: Material.primary
@ -45,6 +45,24 @@ UI.App {
rightPadding: 10 rightPadding: 10
text: Qt.application.name 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 } anchors { left: parent.left; right: parent.right }
spacing: 0 spacing: 0
Rectangle { Label {
id: topItem text: Qt.application.displayName
color: Material.foreground
height: 140 font.pixelSize: UI.Style.fontSizeHeadline
color: UI.Style.primaryColor padding: (homePage.appToolBar.implicitHeight - contentHeight) / 2
leftPadding: 20
Layout.fillWidth: true 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 { Repeater {
id: pageList id: pageList
model: [ 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 { delegate: ItemDelegate {
@ -105,7 +121,7 @@ UI.App {
// Disable, or a double click will push the page twice. // Disable, or a double click will push the page twice.
menuColumn.enabled = false menuColumn.enabled = false
navDrawer.close() navDrawer.close()
pageStack.push(modelData.page) homePage.stack.push(modelData.page)
} }
} }
} }
@ -139,15 +155,26 @@ UI.App {
onTriggered: infoPopup.open() onTriggered: infoPopup.open()
} }
} }
UI.TimePickerCircular {
id: timePicker
}
} }
Component { Component {
id: settingsPage id: settingsPageComponent
UI.AppStackPage { UI.AppStackPage {
id: settingsPage
title: "Settings" title: "Settings"
padding: 0 padding: 0
leftButton: Action {
icon.source: UI.Icons.arrow_back
onTriggered: settingsPage.back()
}
Flickable { Flickable {
contentHeight: settingsPane.implicitHeight contentHeight: settingsPane.implicitHeight
anchors.fill: parent anchors.fill: parent
@ -166,24 +193,29 @@ UI.App {
text: "Display" text: "Display"
} }
UI.SettingsItem { UI.SettingsCheckItem {
title: "Dark Theme" title: "Dark Theme"
check.visible: true checkState: root.theme ? Qt.Checked : Qt.Unchecked
check.checked: root.theme onClicked: root.theme = !root.theme
check.onClicked: root.theme = !root.theme Layout.fillWidth: true
onClicked: check.clicked()
} }
UI.SettingsItem { UI.SettingsItem {
title: "Primary Color" title: "Primary Color"
subtitle: primaryColorPopup.currentColorName subtitle: colorDialog.getColorName(root.primary)
onClicked: primaryColorPopup.open() onClicked: {
colorDialog.selectAccentColor = false
colorDialog.open()
}
} }
UI.SettingsItem { UI.SettingsItem {
title: "Accent Color" title: "Accent Color"
subtitle: accentColorPopup.currentColorName subtitle: colorDialog.getColorName(root.accent)
onClicked: accentColorPopup.open() onClicked: {
colorDialog.selectAccentColor = true
colorDialog.open()
}
} }
} }
} }
@ -192,18 +224,24 @@ UI.App {
} }
Component { Component {
id: aboutPage id: infoPageComponent
UI.AppStackPage { UI.AppStackPage {
title: "About" id: infoPage
title: "Info"
padding: 10 padding: 10
leftButton: Action {
icon.source: UI.Icons.arrow_back
onTriggered: infoPage.back()
}
Flickable { Flickable {
contentHeight: aboutPane.implicitHeight contentHeight: infoPane.implicitHeight
anchors.fill: parent anchors.fill: parent
Pane { Pane {
id: aboutPane id: infoPane
anchors.fill: parent anchors.fill: parent
@ -216,43 +254,11 @@ UI.App {
} }
UI.LabelBody { UI.LabelBody {
property string url: "http://github.com/stemoretti/baseui" text: "<a href='%1'>%1</a>".arg("http://github.com/stemoretti/BaseUI")
text: "<a href='" + url + "'>" + url + "</a>"
linkColor: UI.Style.isDarkTheme ? "lightblue" : "blue" linkColor: UI.Style.isDarkTheme ? "lightblue" : "blue"
onLinkActivated: Qt.openUrlExternally(link) onLinkActivated: Qt.openUrlExternally(link)
horizontalAlignment: Qt.AlignHCenter 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." text: "Information message."
} }
UI.PopupColorSelection { UI.OptionsDialog {
id: primaryColorPopup id: colorDialog
parent: Overlay.overlay property bool selectAccentColor: false
currentColor: root.primary function getColorName(color) {
onColorSelected: function(c) { root.primary = c } var filtered = colorDialog.model.filter((c) => {
} return Material.color(c.bg) === color
})
return filtered.length ? filtered[0].name : ""
}
UI.PopupColorSelection { title: selectAccentColor ? qsTr("Choose accent color") : qsTr("Choose primary color")
id: accentColorPopup 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 Rectangle {
currentColor: root.accent color: Material.color(modelData.bg)
onColorSelected: function(c) { root.accent = c } 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: { 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
import QtQuick.Controls 2.12 import QtQuick.Controls
import BaseUI 1.0
Page { Page {
id: root id: root
property StackView stack: StackView.view readonly property StackView stack: StackView.view
property alias appToolBar: appToolBar property alias appToolBar: appToolBar
property alias leftButton: appToolBar.leftButton property alias leftButton: appToolBar.leftButton
property alias rightButtons: appToolBar.rightButtons property alias rightButtons: appToolBar.rightButtons
function pop(item, operation) { function pop(item, operation) {
if (stack.currentItem != root) if (root.stack.currentItem != root)
return false return false
return root.stack.pop(item, operation)
return stack.pop(item, operation)
} }
function back() { function back() {
@ -23,7 +20,7 @@ Page {
} }
Keys.onBackPressed: function(event) { Keys.onBackPressed: function(event) {
if (stack.depth > 1) { if (root.stack.depth > 1) {
event.accepted = true event.accepted = true
back() back()
} else { } else {
@ -31,15 +28,8 @@ Page {
} }
} }
Action {
id: backAction
icon.source: Icons.arrow_back
onTriggered: root.back()
}
AppToolBar { AppToolBar {
id: appToolBar id: appToolBar
title: root.title title: root.title
leftButton: stack && stack.depth > 1 ? backAction : null
} }
} }

View File

@ -1,8 +1,10 @@
import QtQuick 2.12 import QtQuick
import QtQuick.Layouts 1.12 import QtQuick.Layouts
import QtQuick.Controls 2.12 import QtQuick.Controls
ToolBar { ToolBar {
id: root
property Action leftButton property Action leftButton
property list<Action> rightButtons property list<Action> rightButtons
@ -14,12 +16,12 @@ ToolBar {
anchors { fill: parent; leftMargin: 4; rightMargin: 4 } anchors { fill: parent; leftMargin: 4; rightMargin: 4 }
ToolButton { ToolButton {
icon.source: leftButton ? leftButton.icon.source : "" icon.source: root.leftButton?.icon.source ?? ""
icon.color: Style.textOnPrimary icon.color: Style.textOnPrimary
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
opacity: Style.opacityTitle opacity: Style.opacityTitle
enabled: leftButton && leftButton.enabled enabled: root.leftButton && root.leftButton.enabled
onClicked: leftButton.trigger() onClicked: root.leftButton.trigger()
} }
LabelTitle { LabelTitle {
id: titleLabel id: titleLabel
@ -28,14 +30,14 @@ ToolBar {
Layout.fillWidth: true Layout.fillWidth: true
} }
Repeater { Repeater {
model: rightButtons.length model: root.rightButtons.length
delegate: ToolButton { delegate: ToolButton {
icon.source: rightButtons[index].icon.source icon.source: root.rightButtons[index].icon.source
icon.color: Style.textOnPrimary icon.color: Style.textOnPrimary
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
opacity: Style.opacityTitle opacity: Style.opacityTitle
enabled: rightButtons[index].enabled enabled: root.rightButtons[index].enabled
onClicked: rightButtons[index].trigger() 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
import QtQuick.Layouts 1.12 import QtQuick.Layouts
import QtQuick.Controls 2.12 import QtQuick.Controls
Button { Button {
id: button id: root
property alias textColor: buttonText.color property alias textColor: buttonText.color
property alias buttonColor: buttonBackground.color property alias buttonColor: buttonBackground.color
@ -12,17 +12,16 @@ Button {
leftPadding: 6 leftPadding: 6
rightPadding: 6 rightPadding: 6
Layout.minimumWidth: 80
contentItem: Text { contentItem: Text {
id: buttonText id: buttonText
text: button.text text: root.text
opacity: enabled ? 1.0 : 0.3 opacity: enabled ? 1.0 : 0.3
color: Style.textOnPrimary color: Style.textOnPrimary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
font.capitalization: Font.AllUppercase font.capitalization: Font.AllUppercase
font.weight: Font.Medium
} }
background: Rectangle { background: Rectangle {
@ -30,16 +29,6 @@ Button {
implicitHeight: 48 implicitHeight: 48
color: Style.primaryColor color: Style.primaryColor
radius: 2 radius: 2
opacity: button.pressed ? 0.75 : 1.0 opacity: root.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
}
*/
} }
} }

View File

@ -1,10 +1,9 @@
// ekke (Ekkehard Gentz) @ekkescorner import QtQuick
import QtQuick 2.12 import QtQuick.Layouts
import QtQuick.Layouts 1.12 import QtQuick.Controls
import QtQuick.Controls 2.12
Button { Button {
id: button id: root
property alias textColor: buttonText.color property alias textColor: buttonText.color
@ -12,11 +11,9 @@ Button {
leftPadding: 6 leftPadding: 6
rightPadding: 6 rightPadding: 6
Layout.minimumWidth: 88
contentItem: Text { contentItem: Text {
id: buttonText id: buttonText
text: button.text text: root.text
opacity: enabled ? 1.0 : 0.3 opacity: enabled ? 1.0 : 0.3
color: Style.flatButtonTextColor color: Style.flatButtonTextColor
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -29,8 +26,8 @@ Button {
background: Rectangle { background: Rectangle {
id: buttonBackground id: buttonBackground
implicitHeight: 48 implicitHeight: 48
color: button.pressed ? buttonText.color : "transparent" color: root.pressed ? buttonText.color : "transparent"
radius: 2 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
import QtQuick 2.12 import QtQuick.Layouts
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
Item { Item {
height: 8 height: 8
Layout.fillWidth: true 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 // https://www.google.com/design/spec/components/dividers.html#dividers-types-of-dividers
Rectangle { Rectangle {
anchors.centerIn: parent 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
import QtQuick 2.12 import QtQuick.Layouts
import QtQuick.Layouts 1.12 import QtQuick.Controls
import QtQuick.Controls 2.12
Label { Label {
Layout.fillWidth: true 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
import QtQuick.Window 2.12 import QtQuick.Layouts
import QtQuick.Controls 2.12 import QtQuick.Controls
import QtQuick.Controls.Material 2.12 import QtQuick.Controls.Material
import QtQuick.Layouts 1.12 import QtQuick.Window
import BaseUI 1.0 import BaseUI as UI
Popup { Popup {
id: root id: root
@ -45,13 +45,11 @@ Popup {
RowLayout { RowLayout {
width: parent.width width: parent.width
Image { Icon {
id: alarmIcon width: 36
height: 36
smooth: true icon: UI.Icons.error
source: Icons.error + "color=white" color: "white"
sourceSize.width: 36
sourceSize.height: 36
} }
Label { 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
import QtQuick.Window 2.12 import QtQuick.Controls
import QtQuick.Controls 2.12 import QtQuick.Controls.Material
import QtQuick.Controls.Material 2.12 import QtQuick.Window
Popup { Popup {
id: root 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 pragma Singleton
import QtQuick 2.12 import QtQuick
import QtQuick.Controls.Material 2.12 import QtQuick.Controls.Material
QtObject { QtObject {
property bool isDarkTheme: false 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 <BaseUI/core.h>
#include <QCoreApplication>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickStyle> #include <QQuickStyle>
#include <QDebug>
#include "icons.h" #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 namespace BaseUI
{ {
void init(QQmlEngine *engine) void init(QQmlEngine *engine)
{ {
QQuickStyle::setStyle("Material"); QQuickStyle::setStyle("Material");
Icons::instance = std::make_unique<Icons>();
initialize(engine); #ifdef BASEUI_INCLUDE_ICONS
QString path = ":/baseui/imports/BaseUI/icons/";
qmlRegisterSingletonType<Icons>("BaseUI", 1, 0, "Icons", Icons::singletonProvider); 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 <QJsonObject>
#include <QPainter> #include <QPainter>
#include <QFontMetrics> #include <QFontMetrics>
#include <QVariantMap>
class IconProvider : public QQuickImageProvider class IconProvider : public QQuickImageProvider
{ {
public: 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) : QQuickImageProvider(QQuickImageProvider::Image)
, font(family) , font(family)
{ {
QFile file(codesPath); QFile file(codesPath);
if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto jd = QJsonDocument::fromJson(file.readAll()); QJsonDocument jd = QJsonDocument::fromJson(file.readAll());
if (!jd.isNull()) if (jd.isNull())
codepoints = jd.object();
else
qWarning() << "Invalid codepoints JSON file" << codesPath; qWarning() << "Invalid codepoints JSON file" << codesPath;
else
codepoints = jd.object().toVariantMap();
} else { } else {
qWarning() << "Cannot open icon codes file" << codesPath; qWarning() << "Cannot open icon codes file" << codesPath;
qWarning() << file.errorString(); qWarning() << file.errorString();
@ -44,22 +52,13 @@ public:
if (size) if (size)
*size = QSize(width, height); *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("?"); QString iconChar("?");
if (!args.isEmpty()) { if (id.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 {
qWarning() << "Icon name empty"; 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); font.setPixelSize(width < height ? width : height);
@ -72,29 +71,6 @@ public:
image.fill(Qt::transparent); image.fill(Qt::transparent);
QPainter painter(&image); 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.setFont(font);
painter.drawText(QRect(0, 0, width, height), Qt::AlignCenter, iconChar); painter.drawText(QRect(0, 0, width, height), Qt::AlignCenter, iconChar);
@ -104,7 +80,7 @@ public:
QStringList keys() { return codepoints.keys(); } QStringList keys() { return codepoints.keys(); }
private: private:
QJsonObject codepoints; QVariantMap codepoints;
QFont font; QFont font;
}; };

View File

@ -1,55 +1,52 @@
#include "icons.h" #include "icons.h"
#include <QDebug> #include <QDebug>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QVariant>
#include <QQmlEngine>
#include <QFontDatabase> #include <QFontDatabase>
#include <QColor> #include <QVariantHash>
#include "iconprovider.h" #include "iconprovider.h"
namespace BaseUI
{
Icons::Icons(QObject *parent) Icons::Icons(QObject *parent)
: QQmlPropertyMap(this, parent) : QQmlPropertyMap(this, parent)
{ {
QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership);
} }
Icons *Icons::instance() void Icons::registerIcons(QQmlEngine *engine, const QString &fontPath,
{ const QString &fontName, const QVariantMap &codes)
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)
{ {
QString iconProviderName = "baseui_icons"; QString iconProviderName = "baseui_icons";
if (QFontDatabase::addApplicationFont(path + "MaterialIcons-Regular.ttf") == -1) if (QFontDatabase::addApplicationFont(fontPath) == -1)
qWarning() << "Failed to load font Material"; qWarning() << "Failed to load font:" << fontPath;
auto iconProvider = new IconProvider("Material Icons", path + "codepoints.json");
auto iconProvider = new IconProvider(fontName, codes);
engine->addImageProvider(iconProviderName, iconProvider); 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; QVariantHash hash;
for (const QString &key : iconProvider->keys()) for (const QString &key : iconProvider->keys())
hash.insert(key, QVariant("image://" + iconProviderName + "/" + key + ",")); hash.insert(key, QVariant("image://" + iconProviderName + "/" + key));
instance()->insert(hash); instance->insert(hash);
#endif }
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 #ifndef BASEUI_ICONS_H
#define ICONS_H #define BASEUI_ICONS_H
#include <QQmlEngine>
#include <QQmlPropertyMap> #include <QQmlPropertyMap>
class QQmlEngine; #include <memory>
class QJSEngine;
namespace BaseUI
{
class Icons : public QQmlPropertyMap class Icons : public QQmlPropertyMap
{ {
@ -13,11 +16,13 @@ class Icons : public QQmlPropertyMap
public: public:
Icons(QObject *parent = nullptr); Icons(QObject *parent = nullptr);
static Icons *instance(); inline static std::unique_ptr<Icons> instance;
static QObject *singletonProvider(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
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: protected:
template <typename DerivedType> template <typename DerivedType>
explicit Icons(DerivedType *derived, QObject *parent = nullptr) 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 #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