Warning, /plasma/discover/discover/qml/Rating.qml is written in an unsupported language. File is not indexed.

0001 /*
0002  *   SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
0003  *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 pragma ComponentBehavior: Bound
0008 
0009 import QtQuick
0010 import QtQuick.Controls as QQC2
0011 import org.kde.kirigami as Kirigami
0012 
0013 QQC2.Control {
0014     id: control
0015 
0016     enum EditPolicy {
0017         None = 0,
0018         AllowTapToUnset = 1,
0019         AllowSetZero = 2
0020     }
0021 
0022     enum Precision {
0023         FullStar,
0024         HalfStar
0025     }
0026 
0027     property real starSize: Kirigami.Units.gridUnit
0028     property bool readOnly: true
0029     property /*EditPolicy*/ int editPolicy: Rating.EditPolicy.None
0030     property int /*Precision*/ precision: Rating.Precision.FullStar
0031 
0032     // Allows gracefully handling edits in FullStar precision starting from a HalfStar value.
0033     // For example, starting from value 3 (one and a half star, displayed as 2 full stars),
0034     // with AllowTapToUnset policy clicking on second star will reset value to 0.
0035     readonly property int effectiveValue: precision === Rating.Precision.HalfStar ? value : Math.ceil(value / 2) * 2
0036 
0037     readonly property int hoveredValue: mouseArea.hoveredValue
0038     readonly property bool pressed: mouseArea.pressed
0039 
0040     // Accessible API requires Slider types to have properties named literally like this.
0041     property int value: 0
0042     readonly property int minimumValue: 0
0043     property int maximumValue: 10
0044     readonly property int stepSize: precision === Rating.Precision.HalfStar ? 1 : 2
0045 
0046     signal edited()
0047 
0048     function valueAt(position: real): int {
0049         position = Math.max(0, Math.min(1, position));
0050         let val = 0;
0051         let step = 0;
0052         switch (precision) {
0053         case Rating.Precision.HalfStar:
0054             val = Math.ceil(position * maximumValue);
0055             step = 1;
0056             break;
0057         case Rating.Precision.FullStar:
0058             val = Math.ceil(position * maximumValue / 2) * 2;
0059             step = 2;
0060             break;
0061         }
0062         const min = (editPolicy & Rating.EditPolicy.AllowSetZero) ? 0 : step;
0063         val = Math.max(min, val);
0064         return val;
0065     }
0066 
0067     function __edit(newValue: int) {
0068         if (newValue === value) {
0069             return;
0070         }
0071         value = newValue;
0072         edited();
0073     }
0074 
0075     function decrease() {
0076         if (!readOnly) {
0077             const step = precision === Rating.Precision.HalfStar ? 1 : 2;
0078             const min = (editPolicy & Rating.EditPolicy.AllowSetZero) ? 0 : step;
0079             const newValue = Math.max(min, effectiveValue - step);
0080             __edit(newValue);
0081         }
0082     }
0083 
0084     function increase() {
0085         if (!readOnly) {
0086             const step = precision === Rating.Precision.HalfStar ? 1 : 2;
0087             const newValue = Math.min(maximumValue, effectiveValue + step);
0088             __edit(newValue);
0089         }
0090     }
0091 
0092     Keys.onLeftPressed: event => {
0093         if (readOnly) {
0094             event.accepted = false;
0095         } else {
0096             event.accepted = true;
0097             if (control.mirrored) {
0098                 increase();
0099             } else {
0100                 decrease();
0101             }
0102         }
0103     }
0104 
0105     Keys.onRightPressed: event => {
0106         if (readOnly) {
0107             event.accepted = false;
0108         } else {
0109             event.accepted = true;
0110             if (control.mirrored) {
0111                 decrease();
0112             } else {
0113                 increase();
0114             }
0115         }
0116     }
0117 
0118     Accessible.role: Accessible.Slider
0119     Accessible.description: i18n("Rating")
0120     Accessible.onIncreaseAction: increase()
0121     Accessible.onDecreaseAction: decrease()
0122 
0123     focusPolicy: readOnly ? Qt.NoFocus : Qt.StrongFocus
0124 
0125     hoverEnabled: !readOnly
0126 
0127     padding: Kirigami.Units.smallSpacing
0128     // Reset paddings after qqc2-desktop-style Control
0129     topPadding: undefined
0130     leftPadding: undefined
0131     rightPadding: undefined
0132     bottomPadding: undefined
0133     verticalPadding: undefined
0134     horizontalPadding: undefined
0135 
0136     spacing: 0
0137 
0138     contentItem: Item {
0139         implicitWidth: row.implicitWidth
0140         implicitHeight: row.implicitHeight
0141 
0142         Row {
0143             id: row
0144 
0145             spacing: 0
0146 
0147             LayoutMirroring.enabled: control.mirrored
0148 
0149             Repeater {
0150                 model: Math.ceil(control.maximumValue / 2)
0151 
0152                 Kirigami.Icon {
0153                     required property int index
0154 
0155                     width: control.starSize
0156                     height: control.starSize
0157 
0158                     animated: false
0159 
0160                     source: {
0161                         const base = index * 2;
0162                         const rating = !control.readOnly && control.hovered ? control.hoveredValue : control.effectiveValue;
0163                         if (rating <= base) {
0164                             return "rating-unrated";
0165                         } else if (rating === base + 1 && control.precision === Rating.Precision.HalfStar) {
0166                             return control.mirrored ? "rating-half-rtl" : "rating-half";
0167                         } else {
0168                             // rating >= base + 2
0169                             return "rating";
0170                         }
0171                     }
0172 
0173                     opacity: {
0174                         const base = index * 2;
0175                         const rating = !control.readOnly && control.hovered ? control.hoveredValue : control.effectiveValue;
0176                         if (rating <= base) {
0177                             return 1;
0178                         } else if (!control.readOnly && control.hovered && (control.pressed || control.hoveredValue !== control.effectiveValue)) {
0179                             return 0.7;
0180                         } else {
0181                             return 1;
0182                         }
0183                     }
0184                 }
0185             }
0186         }
0187     }
0188 
0189     // Spans entire control, accounts for paddings
0190     MouseArea {
0191         id: mouseArea
0192 
0193         anchors.fill: parent
0194 
0195         enabled: !control.readOnly
0196 
0197         acceptedButtons: Qt.LeftButton
0198         hoverEnabled: true
0199         // Event stealing prevention seem to be required for it to work in Kirigami.OverlaySheet dialog.
0200         preventStealing: true
0201 
0202         property int hoveredValue: 0
0203 
0204         // Need to differentiate between press+drag vs click
0205         property bool dragging: false
0206         property int dragStartValue: -1
0207 
0208         function initDrag(x: real) {
0209             const value = valueAt(x);
0210             dragStartValue = value;
0211             dragging = false;
0212         }
0213 
0214         function updateDrag(value: int) {
0215             if (value !== dragStartValue) {
0216                 dragging = true;
0217             }
0218         }
0219 
0220         function resetDrag() {
0221             dragStartValue = -1;
0222             dragging = false;
0223         }
0224 
0225         function positionAt(x: real): real {
0226             const visualPosition = (x - control.leftPadding) / (control.width - control.leftPadding - control.rightPadding);
0227             const position = control.mirrored ? 1 - visualPosition : visualPosition;
0228             return position;
0229         }
0230 
0231         function valueAt(x: real): int {
0232             const position = positionAt(x);
0233             const value = control.valueAt(position);
0234             return value;
0235         }
0236 
0237         function setValueAt(x: real) {
0238             let value = valueAt(x);
0239             if (!dragging && (control.editPolicy & Rating.EditPolicy.AllowTapToUnset) && (value === control.effectiveValue)) {
0240                 value = 0;
0241             }
0242             control.__edit(value);
0243         }
0244 
0245         function handleMove(x: real) {
0246             const value = valueAt(mouseX);
0247             hoveredValue = value;
0248             if (pressed) {
0249                 updateDrag(value);
0250             }
0251         }
0252 
0253         onPressed: mouse => {
0254             initDrag(mouse.x);
0255         }
0256 
0257         onReleased: mouse => {
0258             setValueAt(mouse.x);
0259             resetDrag();
0260         }
0261 
0262         onEntered: {
0263             // In some situations entered() signal may not be immediately
0264             // followed by positionChanged(), leading to desync with
0265             // control which does react to mouse enter appropriately.
0266             // Fix this by handling entered() and reading mouseX property.
0267             handleMove(mouseX);
0268         }
0269 
0270         onPositionChanged: mouse => {
0271             handleMove(mouse.x);
0272         }
0273 
0274         onExited: {
0275             hoveredValue = 0;
0276             resetDrag();
0277         }
0278 
0279         onCanceled: {
0280             hoveredValue = 0;
0281             resetDrag();
0282         }
0283     }
0284 
0285     background: Rectangle {
0286         color: "transparent"
0287         border.color: control.Kirigami.Theme.highlightColor
0288         border.width: 1
0289         radius: Kirigami.Units.smallSpacing
0290 
0291         opacity: control.activeFocus && [Qt.TabFocusReason, Qt.BacktabFocusReason].includes(control.focusReason)
0292             ? 1 : 0
0293 
0294         Behavior on opacity {
0295             OpacityAnimator {
0296                 duration: Kirigami.Units.shortDuration
0297                 easing.type: Easing.InOutCubic
0298             }
0299         }
0300     }
0301 }