Changeset View
Changeset View
Standalone View
Standalone View
applets/notifications/package/contents/ui/ThumbnailStrip.qml
Context not available. | |||||
19 | 19 | | |||
---|---|---|---|---|---|
20 | import QtQuick 2.0 | 20 | import QtQuick 2.0 | ||
21 | import QtQuick.Layouts 1.1 | 21 | import QtQuick.Layouts 1.1 | ||
22 | import QtGraphicalEffects 1.0 | ||||
22 | 23 | | |||
23 | import org.kde.plasma.core 2.0 as PlasmaCore | 24 | import org.kde.plasma.core 2.0 as PlasmaCore | ||
24 | import org.kde.plasma.components 2.0 as PlasmaComponents | 25 | import org.kde.plasma.components 2.0 as PlasmaComponents | ||
25 | import org.kde.plasma.extras 2.0 as PlasmaExtras | 26 | import org.kde.plasma.extras 2.0 as PlasmaExtras | ||
26 | 27 | | |||
27 | import org.kde.kquickcontrolsaddons 2.0 | 28 | import org.kde.kquickcontrolsaddons 2.0 as KQCAddons | ||
28 | 29 | | |||
29 | import org.kde.plasma.private.notifications 1.0 as Notifications | 30 | import org.kde.plasma.private.notifications 2.0 as Notifications | ||
30 | 31 | | |||
31 | ListView { | 32 | MouseArea { | ||
32 | id: previewList | 33 | id: thumbnailArea | ||
33 | 34 | | |||
34 | readonly property int itemSquareSize: units.gridUnit * 4 | 35 | // The protocol supports multiple URLs but so far it's only used to show | ||
36 | // a single preview image, so this code is simplified a lot to accomodate | ||||
37 | // this usecase and drops everything else (fallback to app icon or ListView | ||||
38 | // for multiple files) | ||||
39 | property var urls | ||||
35 | 40 | | |||
36 | // if it's only one file, show a larger preview | 41 | readonly property bool dragging: plasmoid.nativeInterface.dragActive | ||
37 | // however if no preview could be generated, which we only know after we tried, | 42 | readonly property alias menuOpen: fileMenu.visible | ||
38 | // the delegate will use itemSquareSize instead as there's no point in showing a huge file icon | | |||
39 | readonly property int itemWidth: previewList.count === 1 ? width : itemSquareSize | | |||
40 | readonly property int itemHeight: previewList.count === 1 ? Math.round(width / 3) : itemSquareSize | | |||
41 | 43 | | |||
42 | // by the time the "model" is populated, the Layout isn't finished yet, causing ListView to have a 0 width | 44 | property int _pressX: -1 | ||
43 | // hence it's based on the mainLayout.width instead | 45 | property int _pressY: -1 | ||
44 | readonly property int maximumItemCount: Math.floor(mainLayout.width / itemSquareSize) | | |||
45 | 46 | | |||
46 | // whether we're currently dragging, this way we can keep the popup around during the entire | 47 | property int leftPadding: 0 | ||
47 | // drag operation even if the mouse leaves the popup | 48 | property int rightPadding: 0 | ||
48 | property bool dragging: Notifications.DragHelper.dragActive | 49 | property int topPadding: 0 | ||
50 | property int bottomPadding: 0 | ||||
49 | 51 | | |||
50 | model: { | 52 | signal openUrl(string url) | ||
51 | var urls = notificationItem.urls | 53 | signal fileActionInvoked | ||
52 | if (urls.length <= maximumItemCount) { | 54 | | ||
53 | return urls | 55 | implicitHeight: Math.max(menuButton.height + 2 * menuButton.anchors.topMargin, | ||
56 | Math.round(Math.min(width / 3, width / thumbnailer.ratio))) | ||||
57 | + topPadding + bottomPadding | ||||
58 | | ||||
59 | preventStealing: true | ||||
60 | cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor | ||||
61 | acceptedButtons: Qt.LeftButton | Qt.RightButton | ||||
62 | | ||||
63 | onClicked: { | ||||
64 | if (mouse.button === Qt.LeftButton) { | ||||
65 | thumbnailArea.openUrl(thumbnailer.url) | ||||
54 | } | 66 | } | ||
55 | // if it wouldn't fit, remove one item in favor of the "+n" badge | | |||
56 | return urls.slice(0, maximumItemCount - 1) | | |||
57 | } | 67 | } | ||
58 | orientation: ListView.Horizontal | | |||
59 | spacing: units.smallSpacing | | |||
60 | interactive: false | | |||
61 | 68 | | |||
62 | footer: notificationItem.urls.length > maximumItemCount ? moreBadge : null | 69 | onPressed: { | ||
70 | if (mouse.button === Qt.LeftButton) { | ||||
71 | _pressX = mouse.x; | ||||
72 | _pressY = mouse.y; | ||||
73 | } else if (mouse.button === Qt.RightButton) { | ||||
74 | // avoid menu button glowing if we didn't actually press it | ||||
75 | menuButton.checked = false; | ||||
63 | 76 | | |||
64 | function pressedAction() { | 77 | fileMenu.visualParent = this; | ||
65 | for (var i = 0; i < count; ++i) { | 78 | fileMenu.open(mouse.x, mouse.y); | ||
66 | var item = itemAtIndex(i) | | |||
67 | if (item.pressed) { | | |||
68 | return item | | |||
69 | } | | |||
70 | } | 79 | } | ||
71 | } | 80 | } | ||
72 | 81 | onPositionChanged: { | |||
73 | // HACK ListView only provides itemAt(x,y) methods but since we don't scroll | 82 | if (_pressX !== -1 && _pressY !== -1 && plasmoid.nativeInterface.isDrag(_pressX, _pressY, mouse.x, mouse.y)) { | ||
74 | // we can make assumptions on what our layout looks like... | 83 | plasmoid.nativeInterface.startDrag(previewPixmap, thumbnailer.url, thumbnailer.pixmap); | ||
75 | function itemAtIndex(index) { | 84 | _pressX = -1; | ||
76 | return itemAt(index * (itemSquareSize + spacing), 0) | 85 | _pressY = -1; | ||
86 | } | ||||
77 | } | 87 | } | ||
78 | 88 | onReleased: { | |||
79 | Component { | 89 | _pressX = -1; | ||
80 | id: moreBadge | 90 | _pressY = -1; | ||
81 | 91 | } | |||
82 | // if there's more urls than we can display, show a "+n" badge | 92 | onContainsMouseChanged: { | ||
83 | Item { | 93 | if (!containsMouse) { | ||
84 | width: moreLabel.width | 94 | _pressX = -1; | ||
85 | height: previewList.height | 95 | _pressY = -1; | ||
86 | | ||||
87 | PlasmaExtras.Heading { | | |||
88 | id: moreLabel | | |||
89 | anchors { | | |||
90 | left: parent.left | | |||
91 | // ListView doesn't add spacing before the footer | | |||
92 | leftMargin: previewList.spacing | | |||
93 | top: parent.top | | |||
94 | bottom: parent.bottom | | |||
95 | } | | |||
96 | level: 3 | | |||
97 | verticalAlignment: Text.AlignVCenter | | |||
98 | text: i18nc("Indicator that there are more urls in the notification than previews shown", "+%1", notificationItem.urls.length - previewList.count) | | |||
99 | } | | |||
100 | } | 96 | } | ||
101 | } | 97 | } | ||
102 | 98 | | |||
103 | delegate: MouseArea { | 99 | Notifications.FileMenu { | ||
104 | id: previewDelegate | 100 | id: fileMenu | ||
105 | 101 | url: thumbnailer.url | |||
106 | property int pressX: -1 | 102 | visualParent: menuButton | ||
107 | property int pressY: -1 | 103 | onActionTriggered: thumbnailArea.fileActionInvoked() | ||
108 | 104 | } | |||
109 | // clip is expensive, only clip if the QPixmapItem would leak outside | | |||
110 | clip: previewPixmap.height > height | | |||
111 | | ||||
112 | width: thumbnailer.hasPreview ? previewList.itemWidth : previewList.itemSquareSize | | |||
113 | height: thumbnailer.hasPreview ? previewList.itemHeight : previewList.itemSquareSize | | |||
114 | | ||||
115 | preventStealing: true | | |||
116 | cursorShape: Qt.OpenHandCursor | | |||
117 | acceptedButtons: Qt.LeftButton | Qt.RightButton | | |||
118 | 105 | | |||
119 | onClicked: { | 106 | Notifications.Thumbnailer { | ||
120 | if (mouse.button === Qt.LeftButton) { | 107 | id: thumbnailer | ||
121 | notificationItem.openUrl(modelData); | | |||
122 | } | | |||
123 | } | | |||
124 | 108 | | |||
125 | onPressed: { | 109 | readonly property real ratio: pixmapSize.height ? pixmapSize.width / pixmapSize.height : 1 | ||
126 | if (mouse.button === Qt.LeftButton) { | | |||
127 | pressX = mouse.x; | | |||
128 | pressY = mouse.y; | | |||
129 | } else if (mouse.button === Qt.RightButton) { | | |||
130 | // avoid menu button glowing if we didn't actually press it | | |||
131 | menuButton.checked = false; | | |||
132 | 110 | | |||
133 | thumbnailer.showContextMenu(mouse.x, mouse.y, modelData, this); | 111 | url: urls[0] | ||
134 | } | 112 | // height is dynamic, so request a "square" size and then show it fitting to aspect ratio | ||
135 | } | 113 | size: Qt.size(thumbnailArea.width, thumbnailArea.width) | ||
136 | onPositionChanged: { | 114 | } | ||
137 | if (pressX !== -1 && pressY !== -1 && Notifications.DragHelper.isDrag(pressX, pressY, mouse.x, mouse.y)) { | | |||
138 | Notifications.DragHelper.startDrag(previewDelegate, modelData /*url*/, thumbnailer.pixmap); | | |||
139 | pressX = -1; | | |||
140 | pressY = -1; | | |||
141 | } | | |||
142 | } | | |||
143 | onReleased: { | | |||
144 | pressX = -1; | | |||
145 | pressY = -1; | | |||
146 | } | | |||
147 | onContainsMouseChanged: { | | |||
148 | if (!containsMouse) { | | |||
149 | pressX = -1; | | |||
150 | pressY = -1; | | |||
151 | } | | |||
152 | } | | |||
153 | 115 | | |||
154 | // first item determines the ListView height | 116 | KQCAddons.QPixmapItem { | ||
155 | Binding { | 117 | id: previewBackground | ||
156 | target: previewList | 118 | anchors.fill: parent | ||
157 | property: "implicitHeight" | 119 | fillMode: Image.PreserveAspectCrop | ||
158 | value: previewDelegate.height | 120 | layer.enabled: true | ||
159 | when: index === 0 | 121 | opacity: 0.25 | ||
122 | pixmap: thumbnailer.pixmap | ||||
123 | layer.effect: FastBlur { | ||||
124 | source: previewBackground | ||||
125 | anchors.fill: parent | ||||
126 | radius: 30 | ||||
160 | } | 127 | } | ||
128 | } | ||||
161 | 129 | | |||
162 | Notifications.Thumbnailer { | 130 | Item { | ||
163 | id: thumbnailer | 131 | anchors { | ||
164 | 132 | fill: parent | |||
165 | readonly property real ratio: pixmapSize.height ? pixmapSize.width / pixmapSize.height : 1 | 133 | leftMargin: thumbnailArea.leftPadding | ||
166 | 134 | rightMargin: thumbnailArea.rightPadding | |||
167 | url: modelData | 135 | topMargin: thumbnailArea.topPadding | ||
168 | size: Qt.size(previewList.itemWidth, previewList.itemHeight) | 136 | bottomMargin: thumbnailArea.bottomPadding | ||
169 | } | 137 | } | ||
170 | 138 | | |||
171 | QPixmapItem { | 139 | KQCAddons.QPixmapItem { | ||
172 | id: previewPixmap | 140 | id: previewPixmap | ||
173 | 141 | anchors.fill: parent | |||
174 | anchors.centerIn: parent | | |||
175 | | ||||
176 | width: parent.width | | |||
177 | height: width / thumbnailer.ratio | | |||
178 | pixmap: thumbnailer.pixmap | 142 | pixmap: thumbnailer.pixmap | ||
179 | smooth: true | 143 | smooth: true | ||
144 | fillMode: Image.PreserveAspectFit | ||||
180 | } | 145 | } | ||
181 | 146 | | |||
182 | PlasmaCore.IconItem { | 147 | PlasmaCore.IconItem { | ||
183 | anchors.fill: parent | 148 | anchors.centerIn: parent | ||
184 | source: thumbnailer.iconName | 149 | width: height | ||
150 | height: units.roundToIconSize(parent.height) | ||||
185 | usesPlasmaTheme: false | 151 | usesPlasmaTheme: false | ||
152 | source: !thumbnailer.busy && !thumbnailer.hasPreview ? thumbnailer.iconName : "" | ||||
186 | } | 153 | } | ||
187 | 154 | | |||
188 | Rectangle { | 155 | PlasmaComponents.BusyIndicator { | ||
189 | anchors { | 156 | anchors.centerIn: parent | ||
190 | left: parent.left | 157 | running: thumbnailer.busy | ||
191 | right: parent.right | 158 | visible: thumbnailer.busy | ||
192 | bottom: parent.bottom | | |||
193 | } | | |||
194 | color: theme.textColor | | |||
195 | opacity: 0.6 | | |||
196 | height: fileNameLabel.contentHeight | | |||
197 | | ||||
198 | PlasmaComponents.Label { | | |||
199 | id: fileNameLabel | | |||
200 | anchors { | | |||
201 | fill: parent | | |||
202 | leftMargin: units.smallSpacing | | |||
203 | rightMargin: units.smallSpacing | | |||
204 | } | | |||
205 | wrapMode: Text.NoWrap | | |||
206 | height: implicitHeight // unset Label default height | | |||
207 | horizontalAlignment: Text.AlignHCenter | | |||
208 | verticalAlignment: Text.AlignVCenter | | |||
209 | elide: Text.ElideMiddle | | |||
210 | font.pointSize: theme.smallestFont.pointSize | | |||
211 | color: theme.backgroundColor | | |||
212 | text: { | | |||
213 | var splitUrl = modelData.split("/") | | |||
214 | return splitUrl[splitUrl.length - 1] | | |||
215 | } | | |||
216 | } | | |||
217 | } | 159 | } | ||
218 | 160 | | |||
219 | PlasmaComponents.Button { | 161 | PlasmaComponents.Button { | ||
Context not available. | |||||
223 | right: parent.right | 165 | right: parent.right | ||
224 | margins: units.smallSpacing | 166 | margins: units.smallSpacing | ||
225 | } | 167 | } | ||
226 | width: Math.ceil(units.iconSizes.small + 2 * units.smallSpacing) | | |||
227 | height: width | | |||
228 | tooltip: i18n("More Options...") | 168 | tooltip: i18n("More Options...") | ||
229 | Accessible.name: tooltip | 169 | Accessible.name: tooltip | ||
170 | iconName: "application-menu" | ||||
230 | checkable: true | 171 | checkable: true | ||
231 | 172 | | |||
232 | // -1 tells it to "align bottom left of item (this)" | 173 | onPressedChanged: { | ||
233 | onClicked: { | 174 | if (pressed) { | ||
234 | checked = Qt.binding(function() { | 175 | // fake "pressed" while menu is open | ||
235 | return thumbnailer.menuVisible; | 176 | checked = Qt.binding(function() { | ||
236 | }); | 177 | return fileMenu.visible; | ||
178 | }); | ||||
237 | 179 | | |||
238 | thumbnailer.showContextMenu(-1, -1, modelData, this) | 180 | fileMenu.visualParent = this; | ||
239 | } | 181 | // -1 tells it to "align bottom left of visualParent (this)" | ||
240 | 182 | fileMenu.open(-1, -1); | |||
241 | PlasmaCore.IconItem { | | |||
242 | anchors { | | |||
243 | fill: parent | | |||
244 | margins: units.smallSpacing | | |||
245 | } | 183 | } | ||
246 | source: "application-menu" | | |||
247 | | ||||
248 | // From Plasma's ToolButtonStyle: | | |||
249 | active: parent.hovered | | |||
250 | colorGroup: parent.hovered ? PlasmaCore.Theme.ButtonColorGroup : PlasmaCore.ColorScope.colorGroup | | |||
251 | } | 184 | } | ||
252 | } | 185 | } | ||
253 | } | 186 | } | ||
Context not available. |