1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Quick Controls module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40import QtQuick 2.2
41import QtQuick.Controls 1.2
42import QtQuick.Controls.Private 1.0
43
44/*!
45    \qmltype CalendarStyle
46    \inqmlmodule QtQuick.Controls.Styles
47    \since 5.3
48    \ingroup controlsstyling
49    \brief Provides custom styling for \l Calendar.
50
51    \section2 Component Map
52
53    \image calendarstyle-components-week-numbers.png
54
55    The calendar has the following styleable components:
56
57    \table
58        \row \li \image square-white.png
59            \li \l background
60            \li Fills the entire control.
61        \row \li \image square-yellow.png
62            \li \l navigationBar
63            \li
64        \row \li \image square-green.png
65            \li \l dayOfWeekDelegate
66            \li One instance per day of week.
67        \row \li \image square-red.png
68            \li \l weekNumberDelegate
69            \li One instance per week.
70        \row \li \image square-blue.png
71            \li \l dayDelegate
72            \li One instance per day of month.
73    \endtable
74
75    \section2 Custom Style Example
76    \qml
77    Calendar {
78        anchors.centerIn: parent
79
80        style: CalendarStyle {
81            gridVisible: false
82            dayDelegate: Rectangle {
83                gradient: Gradient {
84                    GradientStop {
85                        position: 0.00
86                        color: styleData.selected ? "#111" : (styleData.visibleMonth && styleData.valid ? "#444" : "#666");
87                    }
88                    GradientStop {
89                        position: 1.00
90                        color: styleData.selected ? "#444" : (styleData.visibleMonth && styleData.valid ? "#111" : "#666");
91                    }
92                    GradientStop {
93                        position: 1.00
94                        color: styleData.selected ? "#777" : (styleData.visibleMonth && styleData.valid ? "#111" : "#666");
95                    }
96                }
97
98                Label {
99                    text: styleData.date.getDate()
100                    anchors.centerIn: parent
101                    color: styleData.valid ? "white" : "grey"
102                }
103
104                Rectangle {
105                    width: parent.width
106                    height: 1
107                    color: "#555"
108                    anchors.bottom: parent.bottom
109                }
110
111                Rectangle {
112                    width: 1
113                    height: parent.height
114                    color: "#555"
115                    anchors.right: parent.right
116                }
117            }
118        }
119    }
120    \endqml
121*/
122
123Style {
124    id: calendarStyle
125
126    /*!
127        The Calendar this style is attached to.
128    */
129    readonly property Calendar control: __control
130
131    /*!
132        The color of the grid lines.
133    */
134    property color gridColor: "#d3d3d3"
135
136    /*!
137        This property determines the visibility of the grid.
138
139        The default value is \c true.
140    */
141    property bool gridVisible: true
142
143    /*!
144        \internal
145
146        The width of each grid line.
147    */
148    property real __gridLineWidth: 1
149
150    /*! \internal */
151    property color __horizontalSeparatorColor: gridColor
152
153    /*! \internal */
154    property color __verticalSeparatorColor: gridColor
155
156    function __cellRectAt(index) {
157        return CalendarUtils.cellRectAt(index, control.__panel.columns, control.__panel.rows,
158            control.__panel.availableWidth, control.__panel.availableHeight, gridVisible ? __gridLineWidth : 0);
159    }
160
161    function __isValidDate(date) {
162        return date !== undefined
163            && date.getTime() >= control.minimumDate.getTime()
164            && date.getTime() <= control.maximumDate.getTime();
165    }
166
167    /*!
168        The background of the calendar.
169
170        The implicit size of the calendar is calculated based on the implicit size of the background delegate.
171    */
172    property Component background: Rectangle {
173        color: "#fff"
174        implicitWidth: Math.max(250, Math.round(TextSingleton.implicitHeight * 14))
175        implicitHeight: Math.max(250, Math.round(TextSingleton.implicitHeight * 14))
176    }
177
178    /*!
179        The navigation bar of the calendar.
180
181        Styles the bar at the top of the calendar that contains the
182        next month/previous month buttons and the selected date label.
183
184        The properties provided to the delegate are:
185        \table
186            \row \li readonly property string \b styleData.title
187                 \li The title of the calendar.
188        \endtable
189    */
190    property Component navigationBar: Rectangle {
191        height: Math.round(TextSingleton.implicitHeight * 2.73)
192        color: "#f9f9f9"
193
194        Rectangle {
195            color: Qt.rgba(1,1,1,0.6)
196            height: 1
197            width: parent.width
198        }
199
200        Rectangle {
201            anchors.bottom: parent.bottom
202            height: 1
203            width: parent.width
204            color: "#ddd"
205        }
206        HoverButton {
207            id: previousMonth
208            width: parent.height
209            height: width
210            anchors.verticalCenter: parent.verticalCenter
211            anchors.left: parent.left
212            source: "images/leftanglearrow.png"
213            onClicked: control.showPreviousMonth()
214        }
215        Label {
216            id: dateText
217            text: styleData.title
218            elide: Text.ElideRight
219            horizontalAlignment: Text.AlignHCenter
220            font.pixelSize: TextSingleton.implicitHeight * 1.25
221            anchors.verticalCenter: parent.verticalCenter
222            anchors.left: previousMonth.right
223            anchors.leftMargin: 2
224            anchors.right: nextMonth.left
225            anchors.rightMargin: 2
226        }
227        HoverButton {
228            id: nextMonth
229            width: parent.height
230            height: width
231            anchors.verticalCenter: parent.verticalCenter
232            anchors.right: parent.right
233            source: "images/rightanglearrow.png"
234            onClicked: control.showNextMonth()
235        }
236    }
237
238    /*!
239        The delegate that styles each date in the calendar.
240
241        The properties provided to each delegate are:
242        \table
243            \row \li readonly property date \b styleData.date
244                \li The date this delegate represents.
245            \row \li readonly property bool \b styleData.selected
246                \li \c true if this is the selected date.
247            \row \li readonly property int \b styleData.index
248                \li The index of this delegate.
249            \row \li readonly property bool \b styleData.valid
250                \li \c true if this date is greater than or equal to than \l {Calendar::minimumDate}{minimumDate} and
251                    less than or equal to \l {Calendar::maximumDate}{maximumDate}.
252            \row \li readonly property bool \b styleData.today
253                \li \c true if this date is equal to today's date.
254            \row \li readonly property bool \b styleData.visibleMonth
255                \li \c true if the month in this date is the visible month.
256            \row \li readonly property bool \b styleData.hovered
257                \li \c true if the mouse is over this cell.
258                    \note This property is \c true even when the mouse is hovered over an invalid date.
259            \row \li readonly property bool \b styleData.pressed
260                \li \c true if the mouse is pressed on this cell.
261                    \note This property is \c true even when the mouse is pressed on an invalid date.
262        \endtable
263    */
264    property Component dayDelegate: Rectangle {
265        anchors.fill: parent
266        anchors.leftMargin: (!addExtraMargin || control.weekNumbersVisible) && styleData.index % CalendarUtils.daysInAWeek === 0 ? 0 : -1
267        anchors.rightMargin: !addExtraMargin && styleData.index % CalendarUtils.daysInAWeek === CalendarUtils.daysInAWeek - 1 ? 0 : -1
268        anchors.bottomMargin: !addExtraMargin && styleData.index >= CalendarUtils.daysInAWeek * (CalendarUtils.weeksOnACalendarMonth - 1) ? 0 : -1
269        anchors.topMargin: styleData.selected ? -1 : 0
270        color: styleData.date !== undefined && styleData.selected ? selectedDateColor : "transparent"
271
272        readonly property bool addExtraMargin: control.frameVisible && styleData.selected
273        readonly property color sameMonthDateTextColor: "#444"
274        readonly property color selectedDateColor: Qt.platform.os === "osx" ? "#3778d0" : SystemPaletteSingleton.highlight(control.enabled)
275        readonly property color selectedDateTextColor: "white"
276        readonly property color differentMonthDateTextColor: "#bbb"
277        readonly property color invalidDateColor: "#dddddd"
278        Label {
279            id: dayDelegateText
280            text: styleData.date.getDate()
281            anchors.centerIn: parent
282            horizontalAlignment: Text.AlignRight
283            font.pixelSize: Math.min(parent.height/3, parent.width/3)
284            color: {
285                var theColor = invalidDateColor;
286                if (styleData.valid) {
287                    // Date is within the valid range.
288                    theColor = styleData.visibleMonth ? sameMonthDateTextColor : differentMonthDateTextColor;
289                    if (styleData.selected)
290                        theColor = selectedDateTextColor;
291                }
292                theColor;
293            }
294        }
295    }
296
297    /*!
298        The delegate that styles each weekday.
299
300        The height of the weekday row is calculated based on the maximum implicit height of the delegates.
301
302        The properties provided to each delegate are:
303        \table
304            \row \li readonly property int \b styleData.index
305                 \li The index (0-6) of the delegate.
306            \row \li readonly property int \b styleData.dayOfWeek
307                 \li The day of the week this delegate represents. Possible values:
308                     \list
309                     \li \c Locale.Sunday
310                     \li \c Locale.Monday
311                     \li \c Locale.Tuesday
312                     \li \c Locale.Wednesday
313                     \li \c Locale.Thursday
314                     \li \c Locale.Friday
315                     \li \c Locale.Saturday
316                     \endlist
317        \endtable
318    */
319    property Component dayOfWeekDelegate: Rectangle {
320        color: gridVisible ? "#fcfcfc" : "transparent"
321        implicitHeight: Math.round(TextSingleton.implicitHeight * 2.25)
322        Label {
323            text: control.locale.dayName(styleData.dayOfWeek, control.dayOfWeekFormat)
324            anchors.centerIn: parent
325        }
326    }
327
328    /*!
329        The delegate that styles each week number.
330
331        The width of the week number column is calculated based on the maximum implicit width of the delegates.
332
333        The properties provided to each delegate are:
334        \table
335            \row \li readonly property int \b styleData.index
336                 \li The index (0-5) of the delegate.
337            \row \li readonly property int \b styleData.weekNumber
338                 \li The number of the week this delegate represents.
339        \endtable
340    */
341    property Component weekNumberDelegate: Rectangle {
342        implicitWidth: Math.round(TextSingleton.implicitHeight * 2)
343        Label {
344            text: styleData.weekNumber
345            anchors.centerIn: parent
346            color: "#444"
347        }
348    }
349
350    /*! \internal */
351    property Component panel: Item {
352        id: panelItem
353
354        implicitWidth: backgroundLoader.implicitWidth
355        implicitHeight: backgroundLoader.implicitHeight
356
357        property alias navigationBarItem: navigationBarLoader.item
358
359        property alias dayOfWeekHeaderRow: dayOfWeekHeaderRow
360
361        readonly property int weeksToShow: 6
362        readonly property int rows: weeksToShow
363        readonly property int columns: CalendarUtils.daysInAWeek
364
365        // The combined available width and height to be shared amongst each cell.
366        readonly property real availableWidth: viewContainer.width
367        readonly property real availableHeight: viewContainer.height
368
369        property int hoveredCellIndex: -1
370        property int pressedCellIndex: -1
371        property int pressCellIndex: -1
372        property var pressDate: null
373
374        Rectangle {
375            anchors.fill: parent
376            color: "transparent"
377            border.color: gridColor
378            visible: control.frameVisible
379        }
380
381        Item {
382            id: container
383            anchors.fill: parent
384            anchors.margins: control.frameVisible ? 1 : 0
385
386            Loader {
387                id: backgroundLoader
388                anchors.fill: parent
389                sourceComponent: background
390            }
391
392            Loader {
393                id: navigationBarLoader
394                anchors.left: parent.left
395                anchors.right: parent.right
396                anchors.top: parent.top
397                sourceComponent: navigationBar
398                active: control.navigationBarVisible
399
400                property QtObject styleData: QtObject {
401                    readonly property string title: control.locale.standaloneMonthName(control.visibleMonth)
402                        + new Date(control.visibleYear, control.visibleMonth, 1).toLocaleDateString(control.locale, " yyyy")
403                }
404            }
405
406            Row {
407                id: dayOfWeekHeaderRow
408                anchors.top: navigationBarLoader.bottom
409                anchors.left: parent.left
410                anchors.leftMargin: (control.weekNumbersVisible ? weekNumbersItem.width : 0)
411                anchors.right: parent.right
412                spacing: gridVisible ? __gridLineWidth : 0
413                property alias __repeater: repeater
414
415                Repeater {
416                    id: repeater
417                    model: CalendarHeaderModel {
418                        locale: control.locale
419                    }
420                    Loader {
421                        id: dayOfWeekDelegateLoader
422                        sourceComponent: dayOfWeekDelegate
423                        width: __cellRectAt(index).width
424
425                        readonly property int __index: index
426                        readonly property var __dayOfWeek: dayOfWeek
427
428                        property QtObject styleData: QtObject {
429                            readonly property alias index: dayOfWeekDelegateLoader.__index
430                            readonly property alias dayOfWeek: dayOfWeekDelegateLoader.__dayOfWeek
431                        }
432                    }
433                }
434            }
435
436            Rectangle {
437                id: topGridLine
438                color: __horizontalSeparatorColor
439                width: parent.width
440                height: __gridLineWidth
441                visible: gridVisible
442                anchors.top: dayOfWeekHeaderRow.bottom
443            }
444
445            Row {
446                id: gridRow
447                width: weekNumbersItem.width + viewContainer.width
448                height: viewContainer.height
449                anchors.top: topGridLine.bottom
450
451                Column {
452                    id: weekNumbersItem
453                    visible: control.weekNumbersVisible
454                    height: viewContainer.height
455                    spacing: gridVisible ? __gridLineWidth : 0
456                    Repeater {
457                        id: weekNumberRepeater
458                        model: panelItem.weeksToShow
459
460                        Loader {
461                            id: weekNumberDelegateLoader
462                            height: __cellRectAt(index * panelItem.columns).height
463                            sourceComponent: weekNumberDelegate
464
465                            readonly property int __index: index
466                            property int __weekNumber: control.__model.weekNumberAt(index)
467
468                            Connections {
469                                target: control
470
471                                function onVisibleMonthChanged() {
472                                    __weekNumber = control.__model.weekNumberAt(index)
473                                }
474
475                                function onVisibleYearChanged() {
476                                    __weekNumber = control.__model.weekNumberAt(index)
477                                }
478                            }
479
480                            Connections {
481                                target: control.__model
482                                function onCountChanged() {
483                                    __weekNumber = control.__model.weekNumberAt(index)
484                                }
485                            }
486
487                            property QtObject styleData: QtObject {
488                                readonly property alias index: weekNumberDelegateLoader.__index
489                                readonly property int weekNumber: weekNumberDelegateLoader.__weekNumber
490                            }
491                        }
492                    }
493                }
494
495                Rectangle {
496                    id: separator
497                    anchors.topMargin: - dayOfWeekHeaderRow.height - 1
498                    anchors.top: weekNumbersItem.top
499                    anchors.bottom: weekNumbersItem.bottom
500
501                    width: __gridLineWidth
502                    color: __verticalSeparatorColor
503                    visible: control.weekNumbersVisible
504                }
505
506                // Contains the grid lines and the grid itself.
507                Item {
508                    id: viewContainer
509                    width: container.width - (control.weekNumbersVisible ? weekNumbersItem.width + separator.width : 0)
510                    height: container.height - navigationBarLoader.height - dayOfWeekHeaderRow.height - topGridLine.height
511
512                    Repeater {
513                        id: verticalGridLineRepeater
514                        model: panelItem.columns - 1
515                        delegate: Rectangle {
516                            x: __cellRectAt(index + 1).x - __gridLineWidth
517                            y: 0
518                            width: __gridLineWidth
519                            height: viewContainer.height
520                            color: gridColor
521                            visible: gridVisible
522                        }
523                    }
524
525                    Repeater {
526                        id: horizontalGridLineRepeater
527                        model: panelItem.rows - 1
528                        delegate: Rectangle {
529                            x: 0
530                            y: __cellRectAt((index + 1) * panelItem.columns).y - __gridLineWidth
531                            width: viewContainer.width
532                            height: __gridLineWidth
533                            color: gridColor
534                            visible: gridVisible
535                        }
536                    }
537
538                    MouseArea {
539                        id: mouseArea
540                        anchors.fill: parent
541
542                        hoverEnabled: Settings.hoverEnabled
543
544                        function cellIndexAt(mouseX, mouseY) {
545                            var viewContainerPos = viewContainer.mapFromItem(mouseArea, mouseX, mouseY);
546                            var child = viewContainer.childAt(viewContainerPos.x, viewContainerPos.y);
547                            // In the tests, the mouseArea sometimes gets picked instead of the cells,
548                            // probably because stuff is still loading. To be safe, we check for that here.
549                            return child && child !== mouseArea ? child.__index : -1;
550                        }
551
552                        onEntered: {
553                            hoveredCellIndex = cellIndexAt(mouseX, mouseY);
554                            if (hoveredCellIndex === undefined) {
555                                hoveredCellIndex = cellIndexAt(mouseX, mouseY);
556                            }
557
558                            var date = view.model.dateAt(hoveredCellIndex);
559                            if (__isValidDate(date)) {
560                                control.hovered(date);
561                            }
562                        }
563
564                        onExited: {
565                            hoveredCellIndex = -1;
566                        }
567
568                        onPositionChanged: {
569                            var indexOfCell = cellIndexAt(mouse.x, mouse.y);
570                            var previousHoveredCellIndex = hoveredCellIndex;
571                            hoveredCellIndex = indexOfCell;
572                            if (indexOfCell !== -1) {
573                                var date = view.model.dateAt(indexOfCell);
574                                if (__isValidDate(date)) {
575                                    if (hoveredCellIndex !== previousHoveredCellIndex)
576                                        control.hovered(date);
577
578                                    // The date must be different for the pressed signal to be emitted.
579                                    if (pressed && date.getTime() !== control.selectedDate.getTime()) {
580                                        control.pressed(date);
581
582                                        // You can't select dates in a different month while dragging.
583                                        if (date.getMonth() === control.selectedDate.getMonth()) {
584                                            control.selectedDate = date;
585                                            pressedCellIndex = indexOfCell;
586                                        }
587                                    }
588                                }
589                            }
590                        }
591
592                        onPressed: {
593                            pressCellIndex = cellIndexAt(mouse.x, mouse.y);
594                            pressDate = null;
595                            if (pressCellIndex !== -1) {
596                                var date = view.model.dateAt(pressCellIndex);
597                                pressedCellIndex = pressCellIndex;
598                                pressDate = date;
599                                if (__isValidDate(date)) {
600                                    control.selectedDate = date;
601                                    control.pressed(date);
602                                }
603                            }
604                        }
605
606                        onReleased: {
607                            var indexOfCell = cellIndexAt(mouse.x, mouse.y);
608                            if (indexOfCell !== -1) {
609                                // The cell index might be valid, but the date has to be too. We could let the
610                                // selected date validation take care of this, but then the selected date would
611                                // change to the earliest day if a day before the minimum date is clicked, for example.
612                                var date = view.model.dateAt(indexOfCell);
613                                if (__isValidDate(date)) {
614                                    control.released(date);
615                                }
616                            }
617                            pressedCellIndex = -1;
618                        }
619
620                        onClicked: {
621                            var indexOfCell = cellIndexAt(mouse.x, mouse.y);
622                            if (indexOfCell !== -1 && indexOfCell === pressCellIndex) {
623                                if (__isValidDate(pressDate))
624                                    control.clicked(pressDate);
625                            }
626                        }
627
628                        onDoubleClicked: {
629                            var indexOfCell = cellIndexAt(mouse.x, mouse.y);
630                            if (indexOfCell !== -1) {
631                                var date = view.model.dateAt(indexOfCell);
632                                if (__isValidDate(date))
633                                    control.doubleClicked(date);
634                            }
635                        }
636
637                        onPressAndHold: {
638                            var indexOfCell = cellIndexAt(mouse.x, mouse.y);
639                            if (indexOfCell !== -1 && indexOfCell === pressCellIndex) {
640                                var date = view.model.dateAt(indexOfCell);
641                                if (__isValidDate(date))
642                                    control.pressAndHold(date);
643                            }
644                        }
645                    }
646
647                    Connections {
648                        target: control
649                        function onSelectedDateChanged() { view.selectedDateChanged() }
650                    }
651
652                    Repeater {
653                        id: view
654
655                        property int currentIndex: -1
656
657                        model: control.__model
658
659                        Component.onCompleted: selectedDateChanged()
660
661                        function selectedDateChanged() {
662                            if (model !== undefined && model.locale !== undefined) {
663                                currentIndex = model.indexAt(control.selectedDate);
664                            }
665                        }
666
667                        delegate: Loader {
668                            id: delegateLoader
669
670                            x: __cellRectAt(index).x
671                            y: __cellRectAt(index).y
672                            width: __cellRectAt(index).width
673                            height: __cellRectAt(index).height
674                            sourceComponent: dayDelegate
675
676                            readonly property int __index: index
677                            readonly property date __date: date
678                            // We rely on the fact that an invalid QDate will be converted to a Date
679                            // whose year is -4713, which is always an invalid date since our
680                            // earliest minimum date is the year 1.
681                            readonly property bool valid: __isValidDate(date)
682
683                            property QtObject styleData: QtObject {
684                                readonly property alias index: delegateLoader.__index
685                                readonly property bool selected: control.selectedDate.getFullYear() === date.getFullYear() &&
686                                                                 control.selectedDate.getMonth() === date.getMonth() &&
687                                                                 control.selectedDate.getDate() === date.getDate()
688                                readonly property alias date: delegateLoader.__date
689                                readonly property bool valid: delegateLoader.valid
690                                // TODO: this will not be correct if the app is running when a new day begins.
691                                readonly property bool today: date.getTime() === new Date().setHours(0, 0, 0, 0)
692                                readonly property bool visibleMonth: date.getMonth() === control.visibleMonth
693                                readonly property bool hovered: panelItem.hoveredCellIndex == index
694                                readonly property bool pressed: panelItem.pressedCellIndex == index
695                                // todo: pressed property here, clicked and doubleClicked in the control itself
696                            }
697                        }
698                    }
699                }
700            }
701        }
702    }
703}
704