diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 0b141dfe..4e662457 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -142,10 +142,6 @@ Singleton { adapter.location.use12hourFormat = widget.use12HourClock delete widget.use12HourClock } - if (widget.reverseDayMonth !== undefined) { - adapter.location.monthBeforeDay = widget.reverseDayMonth - delete widget.reverseDayMonth - } break } @@ -331,7 +327,6 @@ Singleton { property string name: defaultLocation property bool useFahrenheit: false property bool use12hourFormat: false - property bool monthBeforeDay: false property bool showWeekNumberInCalendar: false } diff --git a/Commons/Time.qml b/Commons/Time.qml index 3c0c67e6..dc294c65 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -15,36 +15,7 @@ Singleton { return Math.floor(date / 1000) } - function formatDate(monthBeforeDay = true) { - let now = date - let dayName = now.toLocaleDateString(Qt.locale(), "ddd") - dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) - let day = now.getDate() - let suffix - if (day > 3 && day < 21) - suffix = 'th' - else - switch (day % 10) { - case 1: - suffix = "st" - break - case 2: - suffix = "nd" - break - case 3: - suffix = "rd" - break - default: - suffix = "th" - } - let month = now.toLocaleDateString(Qt.locale(), "MMMM") - let year = now.toLocaleDateString(Qt.locale(), "yyyy") - - return `${dayName}, ` + (monthBeforeDay ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) - } - - - /** +/** * Formats a Date object into a YYYYMMDD-HHMMSS string. * @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time. * @returns {string} The formatted date string. diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 70a0adea..d4e8b7e0 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -29,167 +29,77 @@ Rectangle { } readonly property string barPosition: Settings.data.bar.position + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" readonly property bool compact: (Settings.data.bar.density === "compact") - readonly property bool use12h: Settings.data.location.use12hourFormat - readonly property bool monthBeforeDay: Settings.data.location.monthBeforeDay + + readonly property var now: Time.date // Resolve settings: try user settings or defaults from BarWidgetRegistry - readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat + readonly property bool usePrimaryColor: widgetSettings.usePrimaryColor !== undefined ? widgetSettings.usePrimaryColor : widgetMetadata.usePrimaryColor + property bool useMonospacedFont: widgetSettings.useMonospacedFont !== undefined ? widgetSettings.useMonospacedFont : widgetMetadata.useMonospacedFont + readonly property string line1: widgetSettings.line1 !== undefined ? widgetSettings.line1 : widgetMetadata.line1 + readonly property string line2: widgetSettings.line2 !== undefined ? widgetSettings.line2 : widgetMetadata.line2 + readonly property string line3: widgetSettings.line3 !== undefined ? widgetSettings.line3 : widgetMetadata.line3 + readonly property string line4: widgetSettings.line4 !== undefined ? widgetSettings.line4 : widgetMetadata.line4 - // Use compact mode for vertical bars - readonly property bool verticalMode: barPosition === "left" || barPosition === "right" - - implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling) - implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill + implicitWidth: isBarVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling) + implicitHeight: isBarVertical ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill radius: Math.round(Style.radiusS * scaling) color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent Item { id: clockContainer - anchors.fill: parent - anchors.margins: compact ? 0 : Style.marginXS * scaling + anchors.centerIn: parent + // anchors.margins: compact ? 0 : Style.marginXS * scaling ColumnLayout { id: layout anchors.centerIn: parent - spacing: verticalMode ? -2 * scaling : -3 * scaling + spacing: isBarVertical ? -2 * scaling : -3 * scaling - // Compact mode for vertical bars - Time section (HH, MM) - Repeater { - model: verticalMode ? 2 : 1 - NText { - readonly property bool showSeconds: (displayFormat === "time-seconds") - readonly property bool inlineDate: (displayFormat === "time-date") - readonly property var now: Time.date - - text: { - if (verticalMode) { - // Compact mode: time section (first 2 lines) - switch (index) { - case 0: - // Hours - if (use12h) { - const hours = now.getHours() - const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours) - return displayHours.toString().padStart(2, '0') - } else { - return now.getHours().toString().padStart(2, '0') - } - case 1: - // Minutes - return now.getMinutes().toString().padStart(2, '0') - default: - return "" - } - } else { - // Normal mode: single line with time - let timeStr = "" - - if (use12h) { - // 12-hour format with proper padding and consistent spacing - const hours = now.getHours() - const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours) - const paddedHours = displayHours.toString().padStart(2, '0') - const minutes = now.getMinutes().toString().padStart(2, '0') - const ampm = hours < 12 ? 'AM' : 'PM' - - if (showSeconds) { - const seconds = now.getSeconds().toString().padStart(2, '0') - timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}` - } else { - timeStr = `${paddedHours}:${minutes} ${ampm}` - } - } else { - // 24-hour format with padding - const hours = now.getHours().toString().padStart(2, '0') - const minutes = now.getMinutes().toString().padStart(2, '0') - - if (showSeconds) { - const seconds = now.getSeconds().toString().padStart(2, '0') - timeStr = `${hours}:${minutes}:${seconds}` - } else { - timeStr = `${hours}:${minutes}` - } - } - - // Add inline date if needed - if (inlineDate) { - let dayName = now.toLocaleDateString(Qt.locale(), "ddd") - dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) - const day = now.getDate().toString().padStart(2, '0') - let month = now.toLocaleDateString(Qt.locale(), "MMM") - timeStr += " - " + (monthBeforeDay ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) - } - - return timeStr - } - } - - font.family: Settings.data.ui.fontFixed - font.pointSize: verticalMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - } - } - - // Separator line for compact mode (between time and date) - Rectangle { - visible: verticalMode - Layout.preferredWidth: 20 * scaling - Layout.preferredHeight: 2 * scaling - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 3 * scaling - Layout.bottomMargin: 3 * scaling - color: Color.mPrimary - opacity: 0.3 - radius: 1 * scaling - } - - // Compact mode for vertical bars - Date section (DD, MM) - Repeater { - model: verticalMode ? 2 : 0 - NText { - readonly property var now: Time.date - - text: { - if (verticalMode) { - // Compact mode: date section (last 2 lines) - switch (index) { - case 0: - return monthBeforeDay ? (now.getMonth() + 1).toString().padStart(2, '0') : now.getDate().toString().padStart(2, '0') - case 1: - return monthBeforeDay ? now.getDate().toString().padStart(2, '0') : (now.getMonth() + 1).toString().padStart(2, '0') - default: - return "" - } - } - return "" - } - - font.pointSize: Style.fontSizeXXS * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - } - } - - // Second line for normal mode (date) NText { - visible: !verticalMode && (displayFormat === "time-date-short") - text: { - const now = Time.date - const day = now.getDate().toString().padStart(2, '0') - const month = (now.getMonth() + 1).toString().padStart(2, '0') - return monthBeforeDay ? `${month}/${day}` : `${day}/${month}` - } + visible: text !== "" + text: Qt.formatDateTime(now, line1.trim()) + font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: isBarVertical ? Style.fontSizeS * scaling : Style.fontSizeS * scaling + font.weight: Style.fontWeightBold + color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } - // Enable fixed-width font for consistent spacing - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeXXS * scaling - font.weight: Style.fontWeightRegular - color: Color.mPrimary + NText { + visible: text !== "" + text: Qt.formatDateTime(now, line2.trim()) + font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: isBarVertical ? Style.fontSizeS * scaling : Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + + NText { + visible: text !== "" && isBarVertical + text: Qt.formatDateTime(now, line3.trim()) + font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: isBarVertical ? Style.fontSizeS * scaling : Style.fontSizeS * scaling + font.weight: Style.fontWeightBold + color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.topMargin: visible ? Style.marginXS * scaling : 0 + } + + NText { + visible: text !== "" && isBarVertical + text: Qt.formatDateTime(now, line4.trim()) + font.family: useMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: isBarVertical ? Style.fontSizeS * scaling : Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter } } @@ -197,7 +107,7 @@ Rectangle { NTooltip { id: tooltip - text: `${Time.formatDate(monthBeforeDay)}.` + text: "Open calendar" target: clockContainer positionAbove: Settings.data.bar.position === "bottom" } diff --git a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml index 3acdf94e..fe65650f 100644 --- a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml @@ -19,7 +19,7 @@ Popup { x: (parent.width - width) * 0.5 y: (parent.height - height) * 0.5 - width: 500 * scaling + width: Math.max(content.implicitWidth + padding * 2, 500 * scaling) height: content.implicitHeight + padding * 2 padding: Style.marginXL * scaling modal: true diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml index 42fc52d5..8b29ec51 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml @@ -8,42 +8,327 @@ import qs.Services ColumnLayout { id: root spacing: Style.marginM * scaling + width: 700 * scaling // Properties to receive data from parent property var widgetData: null property var widgetMetadata: null // Local state - property string valueDisplayFormat: widgetData.displayFormat !== undefined ? widgetData.displayFormat : widgetMetadata.displayFormat + property bool valueUsePrimaryColor: widgetData.usePrimaryColor !== undefined ? widgetData.usePrimaryColor : widgetMetadata.usePrimaryColor + property bool valueUseMonospacedFont: widgetData.useMonospacedFont !== undefined ? widgetData.useMonospacedFont : widgetMetadata.useMonospacedFont + property string valueLine1: widgetData.line1 !== undefined ? widgetData.line1 : widgetMetadata.line1 + property string valueLine2: widgetData.line2 !== undefined ? widgetData.line2 : widgetMetadata.line2 + property string valueLine3: widgetData.line3 !== undefined ? widgetData.line3 : widgetMetadata.line3 + property string valueLine4: widgetData.line4 !== undefined ? widgetData.line4 : widgetMetadata.line4 + + // Track the currently focused input field + property var focusedInput: null + property int focusedLineIndex: -1 + + readonly property var now: Time.date function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.displayFormat = valueDisplayFormat + settings.usePrimaryColor = valueUsePrimaryColor + settings.useMonospacedFont = valueUseMonospacedFont + settings.line1 = valueLine1.trim() + settings.line2 = valueLine2.trim() + settings.line3 = valueLine3.trim() + settings.line4 = valueLine4.trim() return settings } - NComboBox { - label: "Display format" - model: ListModel { - ListElement { - key: "time" - name: "HH:mm" - } - ListElement { - key: "time-seconds" - name: "HH:mm:ss" - } - ListElement { - key: "time-date" - name: "HH:mm + Date" - } - ListElement { - key: "time-date-short" - name: "HH:mm + Short date" + // Function to insert token at cursor position in the focused input + function insertToken(token) { + if (!focusedInput || !focusedInput.inputItem) { + // If no input is focused, default to line 1 + if (inputLine1.inputItem) { + inputLine1.inputItem.focus = true + focusedInput = inputLine1 + focusedLineIndex = 1 } } - currentKey: valueDisplayFormat - onSelected: key => valueDisplayFormat = key - minimumWidth: 230 * scaling + + if (focusedInput && focusedInput.inputItem) { + var input = focusedInput.inputItem + var cursorPos = input.cursorPosition + var currentText = input.text + + // Insert token at cursor position + var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos) + input.text = newText + " " + + // Move cursor after the inserted token + input.cursorPosition = cursorPos + token.length + 1 + + // Ensure the input keeps focus + input.focus = true + } + } + + // Function to update the value property based on which line was edited + function updateLineValue(lineIndex, text) { + switch (lineIndex) { + case 1: + valueLine1 = text + break + case 2: + valueLine2 = text + break + case 3: + valueLine3 = text + break + case 4: + valueLine4 = text + break + } + } + + NToggle { + Layout.fillWidth: true + label: "Use primary color" + description: "When enabled, this applies the primary color for emphasis." + checked: valueUsePrimaryColor + onToggled: checked => valueUsePrimaryColor = checked + } + + NToggle { + Layout.fillWidth: true + label: "Use monospaced font" + description: "When enabled, the clock will use the monospaced font." + checked: valueUseMonospacedFont + onToggled: checked => valueUseMonospacedFont = checked + } + + NDivider { + Layout.fillWidth: true + } + + NHeader { + label: "Clock format" + description: "Build your clock display using the tokens below.\nAdditional lines (3 & 4) are only shown in the vertical bar layout.\nClick on any token to insert it into the selected input field." + } + + RowLayout { + id: main + Layout.fillWidth: true + spacing: Style.marginL * scaling + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 // Equal sizing hint + spacing: Style.marginM * scaling + + NTextInput { + id: inputLine1 + Layout.fillWidth: true + label: "1st line" + placeholderText: "HH:mm" + text: valueLine1 + onTextChanged: updateLineValue(1, text) + + // Track focus state + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputLine1 + root.focusedLineIndex = 1 + } + }) + } + } + } + + NTextInput { + id: inputLine2 + Layout.fillWidth: true + label: "2nd line" + placeholderText: "ddd MMM d" + text: valueLine2 + onTextChanged: updateLineValue(2, text) + + // Track focus state + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputLine2 + root.focusedLineIndex = 2 + } + }) + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 // Equal sizing hint + spacing: Style.marginM * scaling + + NTextInput { + id: inputLine3 + Layout.fillWidth: true + label: "3rd line" + placeholderText: "" + text: valueLine3 + onTextChanged: updateLineValue(3, text) + + // Track focus state + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputLine3 + root.focusedLineIndex = 3 + } + }) + } + } + } + + NTextInput { + id: inputLine4 + Layout.fillWidth: true + label: "4th line" + placeholderText: "" + text: valueLine4 + onTextChanged: updateLineValue(4, text) + + // Track focus state + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputLine4 + root.focusedLineIndex = 4 + } + }) + } + } + } + } + + // -------------- + // Preview + ColumnLayout { + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.fillWidth: false // Don't stretch this column + + NLabel { + label: "Preview" + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + } + + Rectangle { + Layout.preferredWidth: 240 * scaling + Layout.preferredHeight: 120 * scaling // Fixed height instead of fillHeight + + color: Color.mSurfaceVariant + radius: Style.radiusM * scaling + border.color: focusedLineIndex > 0 ? Color.mSecondary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + spacing: -2 * scaling + anchors.centerIn: parent + + NText { + visible: text !== "" + text: Qt.formatDateTime(now, valueLine1.trim()) + font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + NText { + visible: text !== "" + text: Qt.formatDateTime(now, valueLine2.trim()) + font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + NText { + visible: text !== "" + text: Qt.formatDateTime(now, valueLine3.trim()) + font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.topMargin: visible ? Style.marginXS * scaling : 0 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + NText { + visible: text !== "" + text: Qt.formatDateTime(now, valueLine4.trim()) + font.family: valueUseMonospacedFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: valueUsePrimaryColor ? Color.mPrimary : Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + + NDivider { + Layout.topMargin: Style.marginL * scaling + Layout.bottomMargin: Style.marginL * scaling + } + + NHeader { + label: "Tokens" + description: focusedLineIndex > 0 ? "Click any token to add it to line " + focusedLineIndex : "Select an input field above, then click a token to insert it." + } + + NDateTimeTokens { + Layout.fillWidth: true + height: 400 * scaling + + // Connect to token clicked signal if NDateTimeTokens provides it + onTokenClicked: token => root.insertToken(token) } } diff --git a/Modules/SettingsPanel/Tabs/LocationTab.qml b/Modules/SettingsPanel/Tabs/LocationTab.qml index 2dc3fca0..2696abab 100644 --- a/Modules/SettingsPanel/Tabs/LocationTab.qml +++ b/Modules/SettingsPanel/Tabs/LocationTab.qml @@ -93,19 +93,12 @@ ColumnLayout { } NToggle { - label: "Use 12-hour time format" + label: "Use 12-hour time format on the lock screen" description: "On for AM/PM format (e.g., 8:00 PM), off for 24-hour format (e.g., 20:00)." checked: Settings.data.location.use12hourFormat onToggled: checked => Settings.data.location.use12hourFormat = checked } - NToggle { - label: "Show month before day" - description: "On for 09/17/2025, off for 17/09/2025." - checked: Settings.data.location.monthBeforeDay - onToggled: checked => Settings.data.location.monthBeforeDay = checked - } - NToggle { label: "Show week numbers" description: "Displays the week of the year (e.g., Week 38) in the calendar." diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 9783bb4a..38aaea4d 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -53,7 +53,12 @@ Singleton { }, "Clock": { "allowUserSettings": true, - "displayFormat": "time-date-short" + "usePrimaryColor": true, + "useMonospacedFont": true, + "line1": "HH:mm ddd, MMM dd", + "line2": "", + "line3": "", + "line4": "" }, "CustomButton": { "allowUserSettings": true, diff --git a/Widgets/NDateTimeTokens.qml b/Widgets/NDateTimeTokens.qml new file mode 100644 index 00000000..20474e36 --- /dev/null +++ b/Widgets/NDateTimeTokens.qml @@ -0,0 +1,418 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons + +Rectangle { + id: root + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling + + property date sampleDate: new Date() // Dec 25, 2023, 2:30:45.123 PM + + // Signal emitted when a token is clicked + signal tokenClicked(string token) + + ColumnLayout { + id: column + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // Scrollable list of tokens + NScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ListView { + id: tokensList + model: ListModel { + + // Common format combinations + ListElement { + category: "Common" + token: "h:mm AP" + description: "12-hour time with minutes" + example: "2:30 PM" + } + ListElement { + category: "Common" + token: "HH:mm" + description: "24-hour time with minutes" + example: "14:30" + } + ListElement { + category: "Common" + token: "HH:mm:ss" + description: "24-hour time with seconds" + example: "14:30:45" + } + ListElement { + category: "Common" + token: "ddd MMM d" + description: "Weekday, month and day" + example: "Mon Dec 25" + } + ListElement { + category: "Common" + token: "yyyy-MM-dd" + description: "ISO date format" + example: "2023-12-25" + } + ListElement { + category: "Common" + token: "MM/dd/yyyy" + description: "US date format" + example: "12/25/2023" + } + ListElement { + category: "Common" + token: "dd.MM.yyyy" + description: "European date format" + example: "25.12.2023" + } + ListElement { + category: "Common" + token: "ddd, MMM dd" + description: "Weekday with date" + example: "Fri, Dec 12" + } + + // Hour tokens + ListElement { + category: "Hour" + token: "h" + description: "Hour without leading zero (12-hour when used with AP/ap, otherwise 24-hour)" + example: "2 (needs AP/ap for 12hr)" + } + ListElement { + category: "Hour" + token: "hh" + description: "Hour with leading zero (12-hour when used with AP/ap, otherwise 24-hour)" + example: "02 (needs AP/ap for 12hr)" + } + ListElement { + category: "Hour" + token: "h AP" + description: "12-hour format with AM/PM" + example: "2 PM" + } + ListElement { + category: "Hour" + token: "hh AP" + description: "12-hour format with leading zero and AM/PM" + example: "02 PM" + } + ListElement { + category: "Hour" + token: "H" + description: "Hour without leading zero (0-23) - 24-hour format" + example: "14" + } + ListElement { + category: "Hour" + token: "HH" + description: "Hour with leading zero (00-23) - 24-hour format" + example: "14" + } + + // Minute tokens + ListElement { + category: "Minute" + token: "m" + description: "Minute without leading zero (0-59)" + example: "30" + } + ListElement { + category: "Minute" + token: "mm" + description: "Minute with leading zero (00-59)" + example: "30" + } + + // Second tokens + ListElement { + category: "Second" + token: "s" + description: "Second without leading zero (0-59)" + example: "45" + } + ListElement { + category: "Second" + token: "ss" + description: "Second with leading zero (00-59)" + example: "45" + } + + // AM/PM tokens + ListElement { + category: "AM/PM" + token: "AP" + description: "AM/PM in uppercase" + example: "PM" + } + ListElement { + category: "AM/PM" + token: "ap" + description: "am/pm in lowercase" + example: "pm" + } + + // Timezone tokens + ListElement { + category: "Timezone" + token: "t" + description: "Timezone abbreviation" + example: "UTC" + } + + // Year tokens + ListElement { + category: "Year" + token: "yy" + description: "Year as two-digit number (00-99)" + example: "23" + } + ListElement { + category: "Year" + token: "yyyy" + description: "Year as four-digit number" + example: "2023" + } + + // Month tokens + ListElement { + category: "Month" + token: "M" + description: "Month as number without leading zero (1-12)" + example: "12" + } + ListElement { + category: "Month" + token: "MM" + description: "Month as number with leading zero (01-12)" + example: "12" + } + ListElement { + category: "Month" + token: "MMM" + description: "Abbreviated month name" + example: "Dec" + } + ListElement { + category: "Month" + token: "MMMM" + description: "Full month name" + example: "December" + } + + // Day tokens + ListElement { + category: "Day" + token: "d" + description: "Day without leading zero (1-31)" + example: "25" + } + ListElement { + category: "Day" + token: "dd" + description: "Day with leading zero (01-31)" + example: "25" + } + ListElement { + category: "Day" + token: "ddd" + description: "Abbreviated day name" + example: "Mon" + } + ListElement { + category: "Day" + token: "dddd" + description: "Full day name" + example: "Monday" + } + } + + delegate: Rectangle { + id: tokenDelegate + width: tokensList.width + height: 50 * scaling + color: { + if (tokenMouseArea.containsMouse) { + return Qt.alpha(Color.mPrimary, 0.1) + } + return index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant + } + + // Mouse area for the entire delegate + MouseArea { + id: tokenMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + // Emit the signal with the token + root.tokenClicked(model.token) + + // Visual feedback + clickAnimation.start() + } + } + + // Click animation + SequentialAnimation { + id: clickAnimation + PropertyAnimation { + target: tokenDelegate + property: "color" + to: Qt.alpha(Color.mPrimary, 0.3) + duration: 100 + } + PropertyAnimation { + target: tokenDelegate + property: "color" + to: tokenMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : (index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant) + duration: 200 + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginM * scaling + + // Category badge + Rectangle { + width: 70 * scaling + height: 28 * scaling + color: getCategoryColor(model.category)[0] + radius: Style.radiusS * scaling + opacity: tokenMouseArea.containsMouse ? 0.9 : 1.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: model.category + color: getCategoryColor(model.category)[1] + font.pointSize: Style.fontSizeXS * scaling + } + } + + // Token - Made more prominent and clickable + Rectangle { + id: tokenButton + width: 100 * scaling + height: 28 * scaling + color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + radius: Style.radiusS * scaling + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: model.token + color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurface + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightBold + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + // Description + NText { + Layout.fillWidth: true + text: model.description + color: tokenMouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurfaceVariant + font.pointSize: Style.fontSizeS * scaling + wrapMode: Text.WordWrap + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + // Live example + Rectangle { + width: 90 * scaling + height: 28 * scaling + color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant + radius: Style.radiusS * scaling + border.color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: Qt.formatDateTime(root.sampleDate, model.token) + color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurfaceVariant + font.pointSize: Style.fontSizeS * scaling + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } + } + + function getCategoryColor(category) { + switch (category) { + case "Year": + return [Color.mPrimary, Color.mOnPrimary] + case "Month": + return [Color.mSecondary, Color.mOnSecondary] + case "Day": + return [Color.mTertiary, Color.mOnTertiary] + case "Hour": + return [Color.mPrimary, Color.mOnPrimary] + case "Minute": + return [Color.mSecondary, Color.mOnSecondary] + case "Second": + return [Color.mTertiary, Color.mOnTertiary] + case "AM/PM": + return [Color.mError, Color.mOnError] + case "Timezone": + return [Color.mOnSurface, Color.mSurface] + case "Common": + return [Color.mError, Color.mOnError] + default: + return [Color.mOnSurfaceVariant, Color.mSurfaceVariant] + } + } +}