Changeset View
Changeset View
Standalone View
Standalone View
applets/notifications/package/contents/ui/NotificationItem.qml
1 | /* | 1 | /* | ||
---|---|---|---|---|---|
2 | * Copyright 2011 Marco Martin <notmart@gmail.com> | 2 | * Copyright 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de> | ||
3 | * Copyright 2014 Kai Uwe Broulik <kde@privat.broulik.de> | | |||
4 | * | 3 | * | ||
5 | * This program is free software; you can redistribute it and/or modify | 4 | * This program is free software; you can redistribute it and/or | ||
6 | * it under the terms of the GNU Library General Public License as | 5 | * modify it under the terms of the GNU General Public License as | ||
7 | * published by the Free Software Foundation; either version 2, or | 6 | * published by the Free Software Foundation; either version 2 of | ||
8 | * (at your option) any later version. | 7 | * the License or (at your option) version 3 or any later version | ||
8 | * accepted by the membership of KDE e.V. (or its successor approved | ||||
9 | * by the membership of KDE e.V.), which shall act as a proxy | ||||
10 | * defined in Section 14 of version 3 of the license. | ||||
9 | * | 11 | * | ||
10 | * This program is distributed in the hope that it will be useful, | 12 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Library General Public License for more details | 15 | * GNU General Public License for more details. | ||
14 | * | 16 | * | ||
15 | * You should have received a copy of the GNU Library General Public | 17 | * You should have received a copy of the GNU General Public License | ||
16 | * License along with this program; if not, write to the | 18 | * along with this program. If not, see <http://www.gnu.org/licenses/> | ||
17 | * Free Software Foundation, Inc., | | |||
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | | |||
19 | */ | 19 | */ | ||
20 | 20 | | |||
21 | import QtQuick 2.5 | 21 | import QtQuick 2.8 | ||
22 | import QtQuick.Layouts 1.1 | 22 | import QtQuick.Layouts 1.1 | ||
23 | import QtQuick.Controls.Private 1.0 | 23 | import QtQuick.Window 2.2 | ||
24 | 24 | | |||
25 | import org.kde.plasma.core 2.0 as PlasmaCore | 25 | import org.kde.plasma.core 2.0 as PlasmaCore | ||
26 | import org.kde.plasma.components 2.0 as PlasmaComponents | 26 | import org.kde.plasma.components 2.0 as PlasmaComponents | ||
27 | import org.kde.plasma.extras 2.0 as PlasmaExtras | 27 | import org.kde.plasma.extras 2.0 as PlasmaExtras | ||
28 | import org.kde.kquickcontrolsaddons 2.0 | | |||
29 | 28 | | |||
30 | MouseArea { | 29 | import org.kde.kquickcontrolsaddons 2.0 as KQCAddons | ||
31 | id: notificationItem | | |||
32 | width: parent.width | | |||
33 | implicitHeight: { | | |||
34 | if (bodyText.lineCount > 1) { | | |||
35 | return mainLayout.height + 0.5 * units.smallSpacing // close button height = about 1 unit | | |||
36 | } | | |||
37 | if (appIconItem.valid || imageItem.nativeWidth > 0) { | | |||
38 | return Math.max((mainLayout.height + 1.5 * units.smallSpacing),(units.iconSizes.large + 2 * units.smallSpacing)) | | |||
39 | } | | |||
40 | if (bottomPart.height != 0) { | | |||
41 | return mainLayout.height + (mainLayout.height > units.iconSizes.large ? 1.5 : 2) * units.smallSpacing | | |||
42 | } else { | | |||
43 | return mainLayout.height + units.smallSpacing // close button again | | |||
44 | } | | |||
45 | } | | |||
46 | | ||||
47 | // We need to clip here because we support displaying images through <img/> | | |||
48 | // and if we don't clip, they will be painted over the borders of the dialog/item | | |||
49 | clip: true | | |||
50 | | ||||
51 | signal close | | |||
52 | signal configure | | |||
53 | signal action(string actionId) | | |||
54 | signal openUrl(url url) | | |||
55 | 30 | | |||
56 | property bool compact: false | 31 | import org.kde.notificationmanager 1.0 as NotificationManager | ||
57 | 32 | | |||
58 | property alias icon: appIconItem.source | 33 | ColumnLayout { | ||
59 | property alias image: imageItem.image | 34 | id: notificationItem | ||
60 | property alias summary: summaryLabel.text | | |||
61 | property alias body: bodyText.text | | |||
62 | property alias configurable: settingsButton.visible | | |||
63 | property var created | | |||
64 | property var urls: [] | | |||
65 | | ||||
66 | property int maximumTextHeight: -1 | | |||
67 | | ||||
68 | property ListModel actions: ListModel { } | | |||
69 | | ||||
70 | property bool hasDefaultAction: false | | |||
71 | property bool hasConfigureAction: false | | |||
72 | | ||||
73 | readonly property bool dragging: thumbnailStripLoader.item ? thumbnailStripLoader.item.dragging : false | | |||
74 | | ||||
75 | onClicked: { | | |||
76 | // the MEL would close the notification before the action button | | |||
77 | // onClicked handler would fire effectively breaking notification actions | | |||
78 | if (pressedAction()) { | | |||
79 | return | | |||
80 | } | | |||
81 | 35 | | |||
82 | if (hasDefaultAction) { | 36 | property bool hovered: false | ||
83 | // the notification was clicked, trigger the default action if set | 37 | property int maximumLineCount: 0 | ||
84 | action("default") | 38 | property alias bodyCursorShape: bodyLabel.cursorShape | ||
85 | } | | |||
86 | } | | |||
87 | 39 | | |||
88 | function pressedAction() { | 40 | property int notificationType | ||
89 | for (var i = 0, count = actionRepeater.count; i < count; ++i) { | | |||
90 | var item = actionRepeater.itemAt(i) | | |||
91 | if (item.pressed) { | | |||
92 | return item | | |||
93 | } | | |||
94 | } | | |||
95 | 41 | | |||
96 | if (thumbnailStripLoader.item) { | 42 | property bool inGroup: false | ||
97 | var item = thumbnailStripLoader.item.pressedAction() | | |||
98 | if (item) { | | |||
99 | return item | | |||
100 | } | | |||
101 | } | | |||
102 | 43 | | |||
103 | if (settingsButton.pressed) { | 44 | property alias applicationIconSource: notificationHeading.applicationIconSource | ||
104 | return settingsButton | 45 | property alias applicationName: notificationHeading.applicationName | ||
105 | } | 46 | property alias deviceName: notificationHeading.deviceName | ||
106 | 47 | | |||
107 | if (closeButton.pressed) { | 48 | property string summary | ||
108 | return closeButton | 49 | property alias time: notificationHeading.time | ||
109 | } | | |||
110 | 50 | | |||
111 | return null | 51 | property alias configurable: notificationHeading.configurable | ||
112 | } | 52 | property alias dismissable: notificationHeading.dismissable | ||
53 | property alias dismissed: notificationHeading.dismissed | ||||
54 | property alias closable: notificationHeading.closable | ||||
113 | 55 | | |||
114 | function updateTimeLabel() { | 56 | // This isn't an alias because TextEdit RichText adds some HTML tags to it | ||
115 | if (!created || created.getTime() <= 0) { | 57 | property string body | ||
116 | timeLabel.text = "" | 58 | property var icon | ||
117 | return | 59 | property var urls: [] | ||
118 | } | | |||
119 | var currentTime = new Date().getTime() | | |||
120 | var createdTime = created.getTime() | | |||
121 | var d = (currentTime - createdTime) / 1000 | | |||
122 | if (d < 10) { | | |||
123 | timeLabel.text = i18nc("notification was just added, keep short", "Just now") | | |||
124 | } else if (d < 20) { | | |||
125 | timeLabel.text = i18nc("10 seconds ago, keep short", "10 s ago"); | | |||
126 | } else if (d < 40) { | | |||
127 | timeLabel.text = i18nc("30 seconds ago, keep short", "30 s ago"); | | |||
128 | } else if (d < 60 * 60) { | | |||
129 | timeLabel.text = i18ncp("minutes ago, keep short", "%1 min ago", "%1 min ago", Math.round(d / 60)) | | |||
130 | } else if (d <= 60 * 60 * 23) { | | |||
131 | timeLabel.text = Qt.formatTime(created, Qt.locale().timeFormat(Locale.ShortFormat).replace(/.ss?/i, "")) | | |||
132 | } else { | | |||
133 | var yesterday = new Date() | | |||
134 | yesterday.setDate(yesterday.getDate() - 1) // this will wrap | | |||
135 | yesterday.setHours(0) | | |||
136 | yesterday.setMinutes(0) | | |||
137 | yesterday.setSeconds(0) | | |||
138 | 60 | | |||
139 | if (createdTime > yesterday.getTime()) { | 61 | property int jobState | ||
140 | timeLabel.text = i18nc("notification was added yesterday, keep short", "Yesterday"); | 62 | property int percentage | ||
141 | } else { | 63 | property int jobError: 0 | ||
142 | timeLabel.text = i18ncp("notification was added n days ago, keep short", | 64 | property bool suspendable | ||
143 | "%1 day ago", "%1 days ago", | 65 | property bool killable | ||
144 | Math.round((currentTime - yesterday.getTime()) / 1000 / 3600 / 24)); | 66 | | ||
145 | } | 67 | property QtObject jobDetails | ||
146 | } | 68 | property bool showDetails | ||
147 | } | 69 | | ||
70 | property alias configureActionLabel: notificationHeading.configureActionLabel | ||||
71 | property var actionNames: [] | ||||
72 | property var actionLabels: [] | ||||
73 | | ||||
74 | property int headingLeftPadding: 0 | ||||
75 | property int headingRightPadding: 0 | ||||
76 | | ||||
77 | property int thumbnailLeftPadding: 0 | ||||
78 | property int thumbnailRightPadding: 0 | ||||
79 | property int thumbnailTopPadding: 0 | ||||
80 | property int thumbnailBottomPadding: 0 | ||||
81 | | ||||
82 | readonly property bool menuOpen: bodyLabel.contextMenu !== null | ||||
83 | || (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen) | ||||
84 | || (jobLoader.item && jobLoader.item.menuOpen) | ||||
85 | readonly property bool dragging: thumbnailStripLoader.item && thumbnailStripLoader.item.dragging | ||||
86 | | ||||
87 | signal bodyClicked(var mouse) | ||||
88 | signal closeClicked | ||||
89 | signal configureClicked | ||||
90 | signal dismissClicked | ||||
91 | signal actionInvoked(string actionName) | ||||
92 | signal openUrl(string url) | ||||
93 | signal fileActionInvoked | ||||
94 | | ||||
95 | signal suspendJobClicked | ||||
96 | signal resumeJobClicked | ||||
97 | signal killJobClicked | ||||
148 | 98 | | |||
149 | Timer { | 99 | spacing: units.smallSpacing | ||
150 | interval: 15000 | | |||
151 | running: plasmoid.expanded | | |||
152 | repeat: true | | |||
153 | triggeredOnStart: true | | |||
154 | onTriggered: updateTimeLabel() | | |||
155 | } | | |||
156 | 100 | | |||
157 | PlasmaCore.IconItem { | 101 | NotificationHeader { | ||
158 | id: appIconItem | 102 | id: notificationHeading | ||
103 | Layout.fillWidth: true | ||||
104 | Layout.leftMargin: notificationItem.headingLeftPadding | ||||
105 | Layout.rightMargin: notificationItem.headingRightPadding | ||||
159 | 106 | | |||
160 | width: units.iconSizes.large | 107 | inGroup: notificationItem.inGroup | ||
161 | height: units.iconSizes.large | | |||
162 | 108 | | |||
163 | anchors { | 109 | notificationType: notificationItem.notificationType | ||
164 | top: parent.top | 110 | jobState: notificationItem.jobState | ||
165 | left: parent.left | 111 | jobDetails: notificationItem.jobDetails | ||
166 | leftMargin: units.smallSpacing | 112 | | ||
167 | topMargin: units.smallSpacing | 113 | onConfigureClicked: notificationItem.configureClicked() | ||
114 | onDismissClicked: notificationItem.dismissClicked() | ||||
115 | onCloseClicked: notificationItem.closeClicked() | ||||
168 | } | 116 | } | ||
169 | 117 | | |||
170 | visible: imageItem.nativeWidth === 0 && valid | 118 | RowLayout { | ||
171 | animated: false | 119 | id: defaultHeaderContainer | ||
120 | Layout.fillWidth: true | ||||
172 | } | 121 | } | ||
173 | 122 | | |||
174 | QImageItem { | 123 | // Notification body | ||
175 | id: imageItem | 124 | RowLayout { | ||
176 | anchors.fill: appIconItem | 125 | id: bodyRow | ||
177 | 126 | Layout.fillWidth: true | |||
178 | smooth: true | 127 | spacing: units.smallSpacing | ||
179 | visible: nativeWidth > 0 | | |||
180 | } | | |||
181 | 128 | | |||
182 | ColumnLayout { | 129 | ColumnLayout { | ||
183 | id: mainLayout | 130 | Layout.fillWidth: true | ||
184 | 131 | spacing: 0 | |||
185 | anchors { | | |||
186 | top: parent.top | | |||
187 | topMargin: bodyText.lineCount > 1 ? 0 : Math.round((mainLayout.height > units.iconSizes.large ? 0.5 : 1) * units.smallSpacing) // lift up heading if bodyText is too long/tall | | |||
188 | left: appIconItem.valid || imageItem.nativeWidth > 0 ? appIconItem.right : parent.left | | |||
189 | right: parent.right | | |||
190 | leftMargin: units.smallSpacing * 2 | | |||
191 | rightMargin: units.smallSpacing // Equal padding on either side (notification icon margin) | | |||
192 | } | | |||
193 | | ||||
194 | spacing: Math.round(units.smallSpacing / 2) | | |||
195 | 132 | | |||
196 | RowLayout { | 133 | RowLayout { | ||
197 | id: titleBar | 134 | id: summaryRow | ||
198 | spacing: units.smallSpacing | 135 | Layout.fillWidth: true | ||
136 | visible: summaryLabel.text !== "" | ||||
199 | 137 | | |||
200 | PlasmaExtras.Heading { | 138 | PlasmaExtras.Heading { | ||
201 | id: summaryLabel | 139 | id: summaryLabel | ||
202 | Layout.fillWidth: true | 140 | Layout.fillWidth: true | ||
203 | Layout.fillHeight: true | 141 | Layout.preferredHeight: implicitHeight | ||
204 | height: undefined | | |||
205 | verticalAlignment: Text.AlignVCenter | | |||
206 | level: 4 | | |||
207 | elide: Text.ElideRight | | |||
208 | wrapMode: Text.NoWrap | | |||
209 | textFormat: Text.PlainText | 142 | textFormat: Text.PlainText | ||
143 | maximumLineCount: 3 | ||||
144 | wrapMode: Text.WordWrap | ||||
145 | elide: Text.ElideRight | ||||
146 | level: 4 | ||||
147 | text: { | ||||
148 | if (notificationItem.notificationType === NotificationManager.Notifications.JobType) { | ||||
149 | if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) { | ||||
150 | return i18nc("Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary); | ||||
151 | } else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) { | ||||
152 | if (notificationItem.error) { | ||||
153 | if (notificationItem.summary) { | ||||
154 | return i18nc("Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary); | ||||
155 | } else { | ||||
156 | return i18n("Job Failed"); | ||||
210 | } | 157 | } | ||
211 | 158 | } else { | |||
212 | PlasmaExtras.Heading { | 159 | if (notificationItem.summary) { | ||
213 | id: timeLabel | 160 | return i18nc("Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary); | ||
214 | Layout.fillHeight: true | 161 | } else { | ||
215 | level: 5 | 162 | return i18n("Job Finished"); | ||
216 | visible: text !== "" | | |||
217 | verticalAlignment: Text.AlignVCenter | | |||
218 | | ||||
219 | PlasmaCore.ToolTipArea { | | |||
220 | anchors.fill: parent | | |||
221 | subText: Qt.formatDateTime(created, Qt.DefaultLocaleLongDate) | | |||
222 | } | 163 | } | ||
223 | } | 164 | } | ||
224 | | ||||
225 | PlasmaComponents.ToolButton { | | |||
226 | id: settingsButton | | |||
227 | width: units.iconSizes.smallMedium | | |||
228 | height: width | | |||
229 | visible: false | | |||
230 | | ||||
231 | iconSource: "configure" | | |||
232 | | ||||
233 | onClicked: { | | |||
234 | if (notificationItem.hasConfigureAction) { | | |||
235 | notificationItem.action("settings"); | | |||
236 | } else { | | |||
237 | configure() | | |||
238 | } | 165 | } | ||
239 | } | 166 | } | ||
167 | // some apps use their app name as summary, avoid showing the same text twice | ||||
168 | // try very hard to match the two | ||||
169 | if (notificationItem.summary && notificationItem.summary.toLocaleLowerCase().trim() != notificationItem.applicationName.toLocaleLowerCase().trim()) { | ||||
170 | return notificationItem.summary; | ||||
240 | } | 171 | } | ||
241 | 172 | return ""; | |||
242 | PlasmaComponents.ToolButton { | 173 | } | ||
243 | id: closeButton | 174 | visible: text !== "" | ||
244 | | ||||
245 | width: units.iconSizes.smallMedium | | |||
246 | height: width | | |||
247 | flat: compact | | |||
248 | | ||||
249 | iconSource: "window-close" | | |||
250 | | ||||
251 | onClicked: close() | | |||
252 | } | 175 | } | ||
253 | 176 | | |||
177 | // inGroup headerItem is reparented here | ||||
254 | } | 178 | } | ||
255 | 179 | | |||
256 | RowLayout { | 180 | RowLayout { | ||
257 | id: bottomPart | 181 | id: bodyTextRow | ||
258 | Layout.alignment: Qt.AlignTop | 182 | | ||
183 | Layout.fillWidth: true | ||||
259 | spacing: units.smallSpacing | 184 | spacing: units.smallSpacing | ||
260 | 185 | | |||
261 | // Force the whole thing to collapse if the children are invisible | 186 | SelectableLabel { | ||
262 | // If there is a big notification followed by a small one, the height | 187 | id: bodyLabel | ||
263 | // of the popup does not always shrink back, so this forces it to | 188 | // FIXME how to assign this via State? target: bodyLabel.Layout doesn't work and just assigning the property doesn't either | ||
264 | // height=0 when those are invisible. -1 means "default to implicitHeight" | 189 | Layout.alignment: notificationItem.inGroup ? Qt.AlignTop : Qt.AlignVCenter | ||
265 | Layout.maximumHeight: bodyText.length > 0 || notificationItem.actions.count > 0 ? -1 : 0 | 190 | Layout.fillWidth: true | ||
266 | | ||||
267 | PlasmaExtras.ScrollArea { | | |||
268 | id: bodyTextScrollArea | | |||
269 | Layout.alignment: Qt.AlignTop | | |||
270 | Layout.fillWidth: true | | |||
271 | | ||||
272 | implicitHeight: maximumTextHeight > 0 ? Math.min(maximumTextHeight, bodyText.paintedHeight) : bodyText.paintedHeight | | |||
273 | visible: bodyText.length > 0 | | |||
274 | | ||||
275 | flickableItem.boundsBehavior: Flickable.StopAtBounds | | |||
276 | flickableItem.flickableDirection: Flickable.VerticalFlick | | |||
277 | horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff | | |||
278 | | ||||
279 | TextEdit { | | |||
280 | id: bodyText | | |||
281 | width: bodyTextScrollArea.width | | |||
282 | enabled: !Settings.isMobile | | |||
283 | | ||||
284 | color: PlasmaCore.ColorScope.textColor | | |||
285 | selectedTextColor: theme.viewBackgroundColor | | |||
286 | selectionColor: theme.viewFocusColor | | |||
287 | font.capitalization: theme.defaultFont.capitalization | | |||
288 | font.family: theme.defaultFont.family | | |||
289 | font.italic: theme.defaultFont.italic | | |||
290 | font.letterSpacing: theme.defaultFont.letterSpacing | | |||
291 | font.pointSize: theme.defaultFont.pointSize | | |||
292 | font.strikeout: theme.defaultFont.strikeout | | |||
293 | font.underline: theme.defaultFont.underline | | |||
294 | font.weight: theme.defaultFont.weight | | |||
295 | font.wordSpacing: theme.defaultFont.wordSpacing | | |||
296 | renderType: Text.NativeRendering | | |||
297 | selectByMouse: true | | |||
298 | readOnly: true | | |||
299 | wrapMode: Text.Wrap | | |||
300 | textFormat: TextEdit.RichText | | |||
301 | | ||||
302 | // ensure selecting text scrolls the view as needed... | | |||
303 | onCursorRectangleChanged: { | | |||
304 | var flick = bodyTextScrollArea.flickableItem | | |||
305 | if (flick.contentY >= cursorRectangle.y) { | | |||
306 | flick.contentY = cursorRectangle.y | | |||
307 | } else if (flick.contentY + flick.height <= cursorRectangle.y + cursorRectangle.height) { | | |||
308 | flick.contentY = cursorRectangle.y + cursorRectangle.height - flick.height | | |||
309 | } | | |||
310 | } | | |||
311 | MouseArea { | | |||
312 | property int selectionStart | | |||
313 | property point mouseDownPos: Qt.point(-999, -999); | | |||
314 | 191 | | |||
315 | anchors.fill: parent | 192 | Layout.maximumHeight: notificationItem.maximumLineCount > 0 | ||
316 | acceptedButtons: Qt.RightButton | Qt.LeftButton | 193 | ? (theme.mSize(font).height * notificationItem.maximumLineCount) : -1 | ||
317 | cursorShape: bodyText.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor | 194 | text: notificationItem.body | ||
318 | preventStealing: true // don't let us accidentally drag the Flickable | 195 | // Cannot do text !== "" because RichText adds some HTML tags even when empty | ||
319 | 196 | visible: notificationItem.body !== "" | |||
320 | onPressed: { | 197 | onClicked: notificationItem.bodyClicked(mouse) | ||
321 | if (mouse.button === Qt.RightButton) { | 198 | onLinkActivated: Qt.openUrlExternally(link) | ||
322 | contextMenu.link = bodyText.linkAt(mouse.x, mouse.y); | | |||
323 | contextMenu.open(mouse.x, mouse.y); | | |||
324 | return; | | |||
325 | } | | |||
326 | | ||||
327 | mouseDownPos = Qt.point(mouse.x, mouse.y); | | |||
328 | selectionStart = bodyText.positionAt(mouse.x, mouse.y); | | |||
329 | var pos = bodyText.positionAt(mouse.x, mouse.y); | | |||
330 | // deselect() would scroll to the end which we don't want | | |||
331 | bodyText.select(pos, pos); | | |||
332 | } | | |||
333 | | ||||
334 | onReleased: { | | |||
335 | // emulate "onClicked" | | |||
336 | var manhattanLength = Math.abs(mouseDownPos.x - mouse.x) + Math.abs(mouseDownPos.y - mouse.y); | | |||
337 | if (manhattanLength <= Qt.styleHints.startDragDistance) { | | |||
338 | var link = bodyText.linkAt(mouse.x, mouse.y); | | |||
339 | if (link) { | | |||
340 | Qt.openUrlExternally(link); | | |||
341 | } else { | | |||
342 | notificationItem.clicked(null/*mouse*/); | | |||
343 | } | | |||
344 | } | | |||
345 | mouseDownPos = Qt.point(-999, -999); | | |||
346 | } | 199 | } | ||
347 | 200 | | |||
348 | // HACK to be able to select text whilst still getting all mouse events to the MouseArea | 201 | // inGroup iconContainer is reparented here | ||
349 | onPositionChanged: { | | |||
350 | if (pressed) { | | |||
351 | var pos = bodyText.positionAt(mouseX, mouseY); | | |||
352 | if (selectionStart < pos) { | | |||
353 | bodyText.select(selectionStart, pos); | | |||
354 | } else { | | |||
355 | bodyText.select(pos, selectionStart); | | |||
356 | } | | |||
357 | } | 202 | } | ||
358 | } | 203 | } | ||
359 | 204 | | |||
360 | Clipboard { | 205 | Item { | ||
361 | id: clipboard | 206 | id: iconContainer | ||
362 | } | | |||
363 | 207 | | |||
364 | PlasmaComponents.ContextMenu { | 208 | Layout.preferredWidth: units.iconSizes.large | ||
365 | id: contextMenu | 209 | Layout.preferredHeight: units.iconSizes.large | ||
366 | property string link | | |||
367 | 210 | | |||
368 | PlasmaComponents.MenuItem { | 211 | visible: iconItem.active || imageItem.active | ||
369 | text: i18n("Copy Link Address") | | |||
370 | onClicked: clipboard.content = contextMenu.link | | |||
371 | visible: contextMenu.link !== "" | | |||
372 | } | | |||
373 | | ||||
374 | PlasmaComponents.MenuItem { | | |||
375 | separator: true | | |||
376 | visible: contextMenu.link !== "" | | |||
377 | } | | |||
378 | 212 | | |||
379 | PlasmaComponents.MenuItem { | 213 | PlasmaCore.IconItem { | ||
380 | text: i18n("Copy") | 214 | id: iconItem | ||
381 | icon: "edit-copy" | 215 | // don't show two identical icons | ||
382 | enabled: bodyText.selectionStart !== bodyText.selectionEnd | 216 | readonly property bool active: valid && source != notificationItem.applicationIconSource | ||
383 | onClicked: bodyText.copy() | 217 | anchors.fill: parent | ||
218 | usesPlasmaTheme: false | ||||
219 | smooth: true | ||||
220 | source: typeof notificationItem.icon === "string" ? notificationItem.icon : "" | ||||
221 | visible: active | ||||
384 | } | 222 | } | ||
385 | 223 | | |||
386 | PlasmaComponents.MenuItem { | 224 | KQCAddons.QImageItem { | ||
387 | text: i18n("Select All") | 225 | id: imageItem | ||
388 | onClicked: bodyText.selectAll() | 226 | readonly property bool active: !null && nativeWidth > 0 | ||
227 | anchors.fill: parent | ||||
228 | smooth: true | ||||
229 | fillMode: KQCAddons.QImageItem.PreserveAspectFit | ||||
230 | visible: active | ||||
231 | image: typeof notificationItem.icon === "object" ? notificationItem.icon : undefined | ||||
389 | } | 232 | } | ||
390 | } | 233 | } | ||
391 | } | 234 | } | ||
235 | | ||||
236 | // Job progress reporting | ||||
237 | Loader { | ||||
238 | id: jobLoader | ||||
239 | Layout.fillWidth: true | ||||
240 | active: notificationItem.notificationType === NotificationManager.Notifications.JobType | ||||
241 | sourceComponent: JobItem { | ||||
242 | jobState: notificationItem.jobState | ||||
243 | jobError: notificationItem.jobError | ||||
244 | percentage: notificationItem.percentage | ||||
245 | suspendable: notificationItem.suspendable | ||||
246 | killable: notificationItem.killable | ||||
247 | | ||||
248 | jobDetails: notificationItem.jobDetails | ||||
249 | showDetails: notificationItem.showDetails | ||||
250 | | ||||
251 | onSuspendJobClicked: notificationItem.suspendJobClicked() | ||||
252 | onResumeJobClicked: notificationItem.resumeJobClicked() | ||||
253 | onKillJobClicked: notificationItem.killJobClicked() | ||||
254 | | ||||
255 | onOpenUrl: notificationItem.openUrl(url) | ||||
256 | onFileActionInvoked: notificationItem.fileActionInvoked() | ||||
257 | | ||||
258 | hovered: notificationItem.hovered | ||||
392 | } | 259 | } | ||
393 | } | 260 | } | ||
394 | 261 | | |||
395 | ColumnLayout { | 262 | RowLayout { | ||
396 | id: actionsColumn | 263 | Layout.fillWidth: true | ||
397 | Layout.alignment: Qt.AlignTop | 264 | visible: actionRepeater.count > 0 | ||
398 | Layout.maximumWidth: theme.mSize(theme.defaultFont).width * (compact ? 10 : 16) | | |||
399 | // this is so it never collapses but always follows what the Buttons below want | | |||
400 | // but also don't let the buttons get too narrow (e.g. "View" or "Open" button) | | |||
401 | Layout.minimumWidth: Math.max(units.gridUnit * 4, implicitWidth) | | |||
402 | 265 | | |||
266 | // Notification actions | ||||
267 | Flow { // it's a Flow so it can wrap if too long | ||||
268 | Layout.fillWidth: true | ||||
269 | visible: actionRepeater.count > 0 | ||||
403 | spacing: units.smallSpacing | 270 | spacing: units.smallSpacing | ||
404 | visible: notificationItem.actions && notificationItem.actions.count > 0 | 271 | layoutDirection: Qt.RightToLeft | ||
405 | 272 | | |||
406 | Repeater { | 273 | Repeater { | ||
407 | id: actionRepeater | 274 | id: actionRepeater | ||
408 | model: notificationItem.actions | | |||
409 | 275 | | |||
410 | PlasmaComponents.Button { | 276 | model: { | ||
411 | Layout.fillWidth: true | 277 | var buttons = []; | ||
278 | // HACK We want the actions to be right-aligned but Flow also reverses | ||||
279 | var actionNames = (notificationItem.actionNames || []).reverse(); | ||||
280 | var actionLabels = (notificationItem.actionLabels || []).reverse(); | ||||
281 | for (var i = 0; i < actionNames.length; ++i) { | ||||
282 | buttons.push({ | ||||
283 | actionName: actionNames[i], | ||||
284 | label: actionLabels[i] | ||||
285 | }); | ||||
286 | } | ||||
287 | return buttons; | ||||
288 | } | ||||
289 | | ||||
290 | PlasmaComponents.ToolButton { | ||||
291 | flat: false | ||||
292 | // why does it spit "cannot assign undefined to string" when a notification becomes expired? | ||||
293 | text: modelData.label || "" | ||||
412 | Layout.preferredWidth: minimumWidth | 294 | Layout.preferredWidth: minimumWidth | ||
413 | Layout.maximumWidth: actionsColumn.Layout.maximumWidth | 295 | onClicked: notificationItem.actionInvoked(modelData.actionName) | ||
414 | text: model.text | | |||
415 | tooltip: width < minimumWidth ? text : "" | | |||
416 | onClicked: notificationItem.action(model.id) | | |||
417 | } | 296 | } | ||
418 | } | 297 | } | ||
419 | } | 298 | } | ||
420 | } | 299 | } | ||
421 | 300 | | |||
301 | // thumbnails | ||||
422 | Loader { | 302 | Loader { | ||
423 | id: thumbnailStripLoader | 303 | id: thumbnailStripLoader | ||
304 | Layout.leftMargin: notificationItem.thumbnailLeftPadding | ||||
305 | Layout.rightMargin: notificationItem.thumbnailRightPadding | ||||
306 | Layout.topMargin: notificationItem.thumbnailTopPadding | ||||
307 | Layout.bottomMargin: notificationItem.thumbnailBottomPadding | ||||
424 | Layout.fillWidth: true | 308 | Layout.fillWidth: true | ||
425 | Layout.preferredHeight: item ? item.implicitHeight : 0 | | |||
426 | source: "ThumbnailStrip.qml" | | |||
427 | active: notificationItem.urls.length > 0 | 309 | active: notificationItem.urls.length > 0 | ||
310 | visible: active | ||||
311 | sourceComponent: ThumbnailStrip { | ||||
312 | leftPadding: -thumbnailStripLoader.Layout.leftMargin | ||||
313 | rightPadding: -thumbnailStripLoader.Layout.rightMargin | ||||
314 | topPadding: -thumbnailStripLoader.Layout.topMargin | ||||
315 | bottomPadding: -thumbnailStripLoader.Layout.bottomMargin | ||||
316 | urls: notificationItem.urls | ||||
317 | onOpenUrl: notificationItem.openUrl(url) | ||||
318 | onFileActionInvoked: notificationItem.fileActionInvoked() | ||||
319 | } | ||||
320 | } | ||||
321 | | ||||
322 | states: [ | ||||
323 | State { | ||||
324 | when: notificationItem.inGroup | ||||
325 | PropertyChanges { | ||||
326 | target: notificationHeading | ||||
327 | parent: summaryRow | ||||
328 | } | ||||
329 | | ||||
330 | PropertyChanges { | ||||
331 | target: summaryRow | ||||
332 | visible: true | ||||
333 | } | ||||
334 | PropertyChanges { | ||||
335 | target: summaryLabel | ||||
336 | visible: true | ||||
337 | } | ||||
338 | | ||||
339 | /*PropertyChanges { | ||||
340 | target: bodyLabel.Label | ||||
341 | alignment: Qt.AlignTop | ||||
342 | }*/ | ||||
343 | | ||||
344 | PropertyChanges { | ||||
345 | target: iconContainer | ||||
346 | parent: bodyTextRow | ||||
428 | } | 347 | } | ||
429 | } | 348 | } | ||
349 | ] | ||||
430 | } | 350 | } |