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.notificationmanager 1.0 as NotificationManager | ||
30 | | ||||
31 | ColumnLayout { | ||||
31 | id: notificationItem | 32 | 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 | 33 | | |||
47 | // We need to clip here because we support displaying images through <img/> | 34 | property bool hovered: false | ||
48 | // and if we don't clip, they will be painted over the borders of the dialog/item | 35 | property int maximumLineCount: 0 | ||
49 | clip: true | 36 | property alias bodyCursorShape: bodyLabel.cursorShape | ||
50 | 37 | | |||
51 | signal close | 38 | property int notificationType | ||
52 | signal configure | | |||
53 | signal action(string actionId) | | |||
54 | signal openUrl(url url) | | |||
55 | 39 | | |||
56 | property bool compact: false | 40 | property bool inGroup: false | ||
57 | 41 | | |||
58 | property alias icon: appIconItem.source | 42 | property alias applicationIconSource: notificationHeading.applicationIconSource | ||
59 | property alias image: imageItem.image | 43 | property alias applicationName: notificationHeading.applicationName | ||
60 | property alias summary: summaryLabel.text | 44 | property alias deviceName: notificationHeading.deviceName | ||
61 | property alias body: bodyText.text | | |||
62 | property alias configurable: settingsButton.visible | | |||
63 | property var created | | |||
64 | property var urls: [] | | |||
65 | 45 | | |||
66 | property int maximumTextHeight: -1 | 46 | property string summary | ||
47 | property alias time: notificationHeading.time | ||||
67 | 48 | | |||
68 | property ListModel actions: ListModel { } | 49 | property alias configurable: notificationHeading.configurable | ||
50 | property alias dismissable: notificationHeading.dismissable | ||||
51 | property alias dismissed: notificationHeading.dismissed | ||||
52 | property alias closable: notificationHeading.closable | ||||
69 | 53 | | |||
70 | property bool hasDefaultAction: false | 54 | // This isn't an alias because TextEdit RichText adds some HTML tags to it | ||
71 | property bool hasConfigureAction: false | 55 | property string body | ||
56 | property alias icon: iconItem.source | ||||
57 | property var urls: [] | ||||
72 | 58 | | |||
73 | readonly property bool dragging: thumbnailStripLoader.item ? thumbnailStripLoader.item.dragging : false | 59 | property int jobState | ||
60 | property int percentage | ||||
61 | property int error: 0 | ||||
62 | property string errorText | ||||
63 | property bool suspendable | ||||
64 | property bool killable | ||||
65 | | ||||
66 | property QtObject jobDetails | ||||
67 | property bool showDetails | ||||
68 | | ||||
69 | property alias configureActionLabel: notificationHeading.configureActionLabel | ||||
70 | property var actionNames: [] | ||||
71 | property var actionLabels: [] | ||||
72 | | ||||
73 | property int headingLeftPadding: 0 | ||||
74 | property int headingRightPadding: 0 | ||||
75 | | ||||
76 | property int thumbnailLeftPadding: 0 | ||||
77 | property int thumbnailRightPadding: 0 | ||||
78 | property int thumbnailTopPadding: 0 | ||||
79 | property int thumbnailBottomPadding: 0 | ||||
80 | | ||||
81 | readonly property bool menuOpen: bodyLabel.contextMenu !== null | ||||
82 | || (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen) | ||||
83 | || (jobLoader.item && jobLoader.item.menuOpen) | ||||
84 | readonly property bool dragging: thumbnailStripLoader.item && thumbnailStripLoader.item.dragging | ||||
85 | | ||||
86 | signal bodyClicked(var mouse) | ||||
87 | signal closeClicked | ||||
88 | signal configureClicked | ||||
89 | signal dismissClicked | ||||
90 | signal actionInvoked(string actionName) | ||||
91 | signal openUrl(string url) | ||||
92 | signal fileActionInvoked | ||||
93 | | ||||
94 | signal suspendJobClicked | ||||
95 | signal resumeJobClicked | ||||
96 | signal killJobClicked | ||||
74 | 97 | | |||
75 | onClicked: { | 98 | spacing: units.smallSpacing | ||
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 | 99 | | |||
82 | if (hasDefaultAction) { | 100 | NotificationHeader { | ||
83 | // the notification was clicked, trigger the default action if set | 101 | id: notificationHeading | ||
84 | action("default") | 102 | Layout.fillWidth: true | ||
85 | } | 103 | Layout.leftMargin: notificationItem.headingLeftPadding | ||
86 | } | 104 | Layout.rightMargin: notificationItem.headingRightPadding | ||
87 | 105 | | |||
88 | function pressedAction() { | 106 | inGroup: notificationItem.inGroup | ||
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 | 107 | | |||
96 | if (thumbnailStripLoader.item) { | 108 | notificationType: notificationItem.notificationType | ||
97 | var item = thumbnailStripLoader.item.pressedAction() | 109 | jobState: notificationItem.jobState | ||
98 | if (item) { | 110 | jobDetails: notificationItem.jobDetails | ||
99 | return item | 111 | | ||
100 | } | 112 | onConfigureClicked: notificationItem.configureClicked() | ||
113 | onDismissClicked: notificationItem.dismissClicked() | ||||
114 | onCloseClicked: notificationItem.closeClicked() | ||||
101 | } | 115 | } | ||
102 | 116 | | |||
103 | if (settingsButton.pressed) { | 117 | RowLayout { | ||
104 | return settingsButton | 118 | id: defaultHeaderContainer | ||
119 | Layout.fillWidth: true | ||||
105 | } | 120 | } | ||
106 | 121 | | |||
107 | if (closeButton.pressed) { | 122 | // Notification body | ||
108 | return closeButton | 123 | RowLayout { | ||
109 | } | 124 | id: bodyRow | ||
125 | Layout.fillWidth: true | ||||
126 | spacing: units.smallSpacing | ||||
110 | 127 | | |||
111 | return null | 128 | ColumnLayout { | ||
112 | } | 129 | Layout.fillWidth: true | ||
130 | spacing: 0 | ||||
113 | 131 | | |||
114 | function updateTimeLabel() { | 132 | RowLayout { | ||
115 | if (!created || created.getTime() <= 0) { | 133 | id: summaryRow | ||
116 | timeLabel.text = "" | 134 | Layout.fillWidth: true | ||
117 | return | 135 | visible: summaryLabel.text !== "" | ||
136 | | ||||
137 | PlasmaExtras.Heading { | ||||
138 | id: summaryLabel | ||||
139 | Layout.fillWidth: true | ||||
140 | Layout.preferredHeight: implicitHeight | ||||
141 | textFormat: Text.PlainText | ||||
142 | maximumLineCount: 3 | ||||
143 | wrapMode: Text.WordWrap | ||||
144 | elide: Text.ElideRight | ||||
145 | level: 4 | ||||
146 | text: { | ||||
147 | if (notificationItem.notificationType === NotificationManager.Notifications.JobType) { | ||||
148 | if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) { | ||||
149 | return i18nc("Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary); | ||||
150 | } else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) { | ||||
151 | if (notificationItem.error) { | ||||
152 | if (notificationItem.summary) { | ||||
153 | return i18nc("Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary); | ||||
154 | } else { | ||||
155 | return i18n("Job Failed"); | ||||
118 | } | 156 | } | ||
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 { | 157 | } else { | ||
133 | var yesterday = new Date() | 158 | if (notificationItem.summary) { | ||
134 | yesterday.setDate(yesterday.getDate() - 1) // this will wrap | 159 | return i18nc("Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary); | ||
135 | yesterday.setHours(0) | | |||
136 | yesterday.setMinutes(0) | | |||
137 | yesterday.setSeconds(0) | | |||
138 | | ||||
139 | if (createdTime > yesterday.getTime()) { | | |||
140 | timeLabel.text = i18nc("notification was added yesterday, keep short", "Yesterday"); | | |||
141 | } else { | 160 | } else { | ||
142 | timeLabel.text = i18ncp("notification was added n days ago, keep short", | 161 | return i18n("Job Finished"); | ||
143 | "%1 day ago", "%1 days ago", | | |||
144 | Math.round((currentTime - yesterday.getTime()) / 1000 / 3600 / 24)); | | |||
145 | } | 162 | } | ||
146 | } | 163 | } | ||
147 | } | 164 | } | ||
148 | | ||||
149 | Timer { | | |||
150 | interval: 15000 | | |||
151 | running: plasmoid.expanded | | |||
152 | repeat: true | | |||
153 | triggeredOnStart: true | | |||
154 | onTriggered: updateTimeLabel() | | |||
155 | } | 165 | } | ||
156 | 166 | // some apps use their app name as summary, avoid showing the same text twice | |||
157 | PlasmaCore.IconItem { | 167 | // try very hard to match the two | ||
158 | id: appIconItem | 168 | if (notificationItem.summary && notificationItem.summary.toLocaleLowerCase().trim() != notificationItem.applicationName.toLocaleLowerCase().trim()) { | ||
159 | 169 | return notificationItem.summary; | |||
160 | width: units.iconSizes.large | | |||
161 | height: units.iconSizes.large | | |||
162 | | ||||
163 | anchors { | | |||
164 | top: parent.top | | |||
165 | left: parent.left | | |||
166 | leftMargin: units.smallSpacing | | |||
167 | topMargin: units.smallSpacing | | |||
168 | } | 170 | } | ||
169 | 171 | return ""; | |||
170 | visible: imageItem.nativeWidth === 0 && valid | | |||
171 | animated: false | | |||
172 | } | 172 | } | ||
173 | 173 | visible: text !== "" | |||
174 | QImageItem { | | |||
175 | id: imageItem | | |||
176 | anchors.fill: appIconItem | | |||
177 | | ||||
178 | smooth: true | | |||
179 | visible: nativeWidth > 0 | | |||
180 | } | 174 | } | ||
181 | 175 | | |||
182 | ColumnLayout { | 176 | // inGroup headerItem is reparented here | ||
183 | id: mainLayout | | |||
184 | | ||||
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 | } | 177 | } | ||
193 | 178 | | |||
194 | spacing: Math.round(units.smallSpacing / 2) | | |||
195 | | ||||
196 | RowLayout { | 179 | RowLayout { | ||
197 | id: titleBar | 180 | id: bodyTextRow | ||
181 | | ||||
182 | Layout.fillWidth: true | ||||
198 | spacing: units.smallSpacing | 183 | spacing: units.smallSpacing | ||
199 | 184 | | |||
200 | PlasmaExtras.Heading { | 185 | SelectableLabel { | ||
201 | id: summaryLabel | 186 | id: bodyLabel | ||
187 | // FIXME how to assign this via State? target: bodyLabel.Layout doesn't work and just assigning the property doesn't either | ||||
188 | Layout.alignment: notificationItem.inGroup ? Qt.AlignTop : Qt.AlignVCenter | ||||
202 | Layout.fillWidth: true | 189 | Layout.fillWidth: true | ||
203 | Layout.fillHeight: true | | |||
204 | height: undefined | | |||
205 | verticalAlignment: Text.AlignVCenter | | |||
206 | level: 4 | | |||
207 | elide: Text.ElideRight | | |||
208 | wrapMode: Text.NoWrap | | |||
209 | textFormat: Text.PlainText | | |||
210 | } | | |||
211 | 190 | | |||
212 | PlasmaExtras.Heading { | 191 | Layout.maximumHeight: notificationItem.maximumLineCount > 0 | ||
213 | id: timeLabel | 192 | ? (theme.mSize(font).height * notificationItem.maximumLineCount) : -1 | ||
214 | Layout.fillHeight: true | 193 | text: notificationItem.body | ||
215 | level: 5 | 194 | // Cannot do text !== "" because RichText adds some HTML tags even when empty | ||
216 | visible: text !== "" | 195 | visible: notificationItem.body !== "" | ||
217 | verticalAlignment: Text.AlignVCenter | 196 | onClicked: notificationItem.bodyClicked(mouse) | ||
197 | onLinkActivated: Qt.openUrlExternally(link) | ||||
198 | } | ||||
218 | 199 | | |||
219 | PlasmaCore.ToolTipArea { | 200 | // inGroup IconItem is reparented here | ||
220 | anchors.fill: parent | | |||
221 | subText: Qt.formatDateTime(created, Qt.DefaultLocaleLongDate) | | |||
222 | } | 201 | } | ||
223 | } | 202 | } | ||
224 | 203 | | |||
225 | PlasmaComponents.ToolButton { | 204 | PlasmaCore.IconItem { | ||
226 | id: settingsButton | 205 | id: iconItem | ||
227 | width: units.iconSizes.smallMedium | 206 | Layout.preferredWidth: units.iconSizes.large | ||
228 | height: width | 207 | Layout.preferredHeight: units.iconSizes.large | ||
229 | visible: false | 208 | usesPlasmaTheme: false | ||
230 | 209 | smooth: true | |||
231 | iconSource: "configure" | 210 | // don't show two identical icons | ||
232 | 211 | visible: valid && source != notificationItem.applicationIconSource | |||
233 | onClicked: { | | |||
234 | if (notificationItem.hasConfigureAction) { | | |||
235 | notificationItem.action("settings"); | | |||
236 | } else { | | |||
237 | configure() | | |||
238 | } | | |||
239 | } | 212 | } | ||
240 | } | 213 | } | ||
241 | 214 | | |||
242 | PlasmaComponents.ToolButton { | 215 | // Job progress reporting | ||
243 | id: closeButton | 216 | Loader { | ||
244 | 217 | id: jobLoader | |||
245 | width: units.iconSizes.smallMedium | 218 | Layout.fillWidth: true | ||
246 | height: width | 219 | active: notificationItem.notificationType === NotificationManager.Notifications.JobType | ||
247 | flat: compact | 220 | sourceComponent: JobItem { | ||
221 | jobState: notificationItem.jobState | ||||
222 | error: notificationItem.error | ||||
223 | errorText: notificationItem.errorText | ||||
224 | percentage: notificationItem.percentage | ||||
225 | suspendable: notificationItem.suspendable | ||||
226 | killable: notificationItem.killable | ||||
227 | | ||||
228 | jobDetails: notificationItem.jobDetails | ||||
229 | showDetails: notificationItem.showDetails | ||||
230 | | ||||
231 | onSuspendJobClicked: notificationItem.suspendJobClicked() | ||||
232 | onResumeJobClicked: notificationItem.resumeJobClicked() | ||||
233 | onKillJobClicked: notificationItem.killJobClicked() | ||||
248 | 234 | | |||
249 | iconSource: "window-close" | 235 | onOpenUrl: notificationItem.openUrl(url) | ||
236 | onFileActionInvoked: notificationItem.fileActionInvoked() | ||||
250 | 237 | | |||
251 | onClicked: close() | 238 | hovered: notificationItem.hovered | ||
252 | } | 239 | } | ||
253 | | ||||
254 | } | 240 | } | ||
255 | 241 | | |||
256 | RowLayout { | 242 | RowLayout { | ||
257 | id: bottomPart | 243 | Layout.fillWidth: true | ||
258 | Layout.alignment: Qt.AlignTop | 244 | visible: actionRepeater.count > 0 | ||
245 | | ||||
246 | // Notification actions | ||||
247 | Flow { // it's a Flow so it can wrap if too long | ||||
248 | Layout.fillWidth: true | ||||
249 | visible: actionRepeater.count > 0 | ||||
259 | spacing: units.smallSpacing | 250 | spacing: units.smallSpacing | ||
251 | layoutDirection: Qt.RightToLeft | ||||
260 | 252 | | |||
261 | // Force the whole thing to collapse if the children are invisible | 253 | Repeater { | ||
262 | // If there is a big notification followed by a small one, the height | 254 | id: actionRepeater | ||
263 | // of the popup does not always shrink back, so this forces it to | 255 | | ||
264 | // height=0 when those are invisible. -1 means "default to implicitHeight" | 256 | model: { | ||
265 | Layout.maximumHeight: bodyText.length > 0 || notificationItem.actions.count > 0 ? -1 : 0 | 257 | var buttons = []; | ||
266 | 258 | // HACK We want the actions to be right-aligned but Flow also reverses | |||
267 | PlasmaExtras.ScrollArea { | 259 | var actionNames = (notificationItem.actionNames || []).reverse(); | ||
268 | id: bodyTextScrollArea | 260 | var actionLabels = (notificationItem.actionLabels || []).reverse(); | ||
269 | Layout.alignment: Qt.AlignTop | 261 | for (var i = 0; i < actionNames.length; ++i) { | ||
270 | Layout.fillWidth: true | 262 | buttons.push({ | ||
271 | 263 | actionName: actionNames[i], | |||
272 | implicitHeight: maximumTextHeight > 0 ? Math.min(maximumTextHeight, bodyText.paintedHeight) : bodyText.paintedHeight | 264 | label: actionLabels[i] | ||
273 | visible: bodyText.length > 0 | 265 | }); | ||
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 | | ||||
315 | anchors.fill: parent | | |||
316 | acceptedButtons: Qt.RightButton | Qt.LeftButton | | |||
317 | cursorShape: bodyText.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor | | |||
318 | preventStealing: true // don't let us accidentally drag the Flickable | | |||
319 | | ||||
320 | onPressed: { | | |||
321 | if (mouse.button === Qt.RightButton) { | | |||
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 | } | 266 | } | ||
345 | mouseDownPos = Qt.point(-999, -999); | 267 | return buttons; | ||
346 | } | 268 | } | ||
347 | 269 | | |||
348 | // HACK to be able to select text whilst still getting all mouse events to the MouseArea | 270 | PlasmaComponents.ToolButton { | ||
349 | onPositionChanged: { | 271 | flat: false | ||
350 | if (pressed) { | 272 | // why does it spit "cannot assign undefined to string" when a notification becomes expired? | ||
351 | var pos = bodyText.positionAt(mouseX, mouseY); | 273 | text: modelData.label || "" | ||
352 | if (selectionStart < pos) { | 274 | Layout.preferredWidth: minimumWidth | ||
353 | bodyText.select(selectionStart, pos); | 275 | onClicked: notificationItem.actionInvoked(modelData.actionName) | ||
354 | } else { | | |||
355 | bodyText.select(pos, selectionStart); | | |||
356 | } | 276 | } | ||
357 | } | 277 | } | ||
358 | } | 278 | } | ||
359 | | ||||
360 | Clipboard { | | |||
361 | id: clipboard | | |||
362 | } | 279 | } | ||
363 | 280 | | |||
364 | PlasmaComponents.ContextMenu { | 281 | // thumbnails | ||
365 | id: contextMenu | 282 | Loader { | ||
366 | property string link | 283 | id: thumbnailStripLoader | ||
367 | 284 | Layout.leftMargin: notificationItem.thumbnailLeftPadding | |||
368 | PlasmaComponents.MenuItem { | 285 | Layout.rightMargin: notificationItem.thumbnailRightPadding | ||
369 | text: i18n("Copy Link Address") | 286 | Layout.topMargin: notificationItem.thumbnailTopPadding | ||
370 | onClicked: clipboard.content = contextMenu.link | 287 | Layout.bottomMargin: notificationItem.thumbnailBottomPadding | ||
371 | visible: contextMenu.link !== "" | 288 | Layout.fillWidth: true | ||
289 | active: notificationItem.urls.length > 0 | ||||
290 | visible: active | ||||
291 | sourceComponent: ThumbnailStrip { | ||||
292 | leftPadding: -thumbnailStripLoader.Layout.leftMargin | ||||
293 | rightPadding: -thumbnailStripLoader.Layout.rightMargin | ||||
294 | topPadding: -thumbnailStripLoader.Layout.topMargin | ||||
295 | bottomPadding: -thumbnailStripLoader.Layout.bottomMargin | ||||
296 | urls: notificationItem.urls | ||||
297 | onOpenUrl: notificationItem.openUrl(url) | ||||
298 | onFileActionInvoked: notificationItem.fileActionInvoked() | ||||
372 | } | 299 | } | ||
373 | | ||||
374 | PlasmaComponents.MenuItem { | | |||
375 | separator: true | | |||
376 | visible: contextMenu.link !== "" | | |||
377 | } | 300 | } | ||
378 | 301 | | |||
379 | PlasmaComponents.MenuItem { | 302 | states: [ | ||
380 | text: i18n("Copy") | 303 | State { | ||
381 | icon: "edit-copy" | 304 | when: notificationItem.inGroup | ||
382 | enabled: bodyText.selectionStart !== bodyText.selectionEnd | 305 | PropertyChanges { | ||
383 | onClicked: bodyText.copy() | 306 | target: notificationHeading | ||
307 | parent: summaryRow | ||||
384 | } | 308 | } | ||
385 | 309 | | |||
386 | PlasmaComponents.MenuItem { | 310 | PropertyChanges { | ||
387 | text: i18n("Select All") | 311 | target: summaryRow | ||
388 | onClicked: bodyText.selectAll() | 312 | visible: true | ||
389 | } | | |||
390 | } | | |||
391 | } | | |||
392 | } | 313 | } | ||
314 | PropertyChanges { | ||||
315 | target: summaryLabel | ||||
316 | visible: true | ||||
393 | } | 317 | } | ||
394 | 318 | | |||
395 | ColumnLayout { | 319 | /*PropertyChanges { | ||
396 | id: actionsColumn | 320 | target: bodyLabel.Label | ||
397 | Layout.alignment: Qt.AlignTop | 321 | alignment: Qt.AlignTop | ||
398 | Layout.maximumWidth: theme.mSize(theme.defaultFont).width * (compact ? 10 : 16) | 322 | }*/ | ||
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 | 323 | | |||
403 | spacing: units.smallSpacing | 324 | PropertyChanges { | ||
404 | visible: notificationItem.actions && notificationItem.actions.count > 0 | 325 | target: iconItem | ||
405 | 326 | parent: bodyTextRow | |||
406 | Repeater { | | |||
407 | id: actionRepeater | | |||
408 | model: notificationItem.actions | | |||
409 | | ||||
410 | PlasmaComponents.Button { | | |||
411 | Layout.fillWidth: true | | |||
412 | Layout.preferredWidth: minimumWidth | | |||
413 | Layout.maximumWidth: actionsColumn.Layout.maximumWidth | | |||
414 | text: model.text | | |||
415 | tooltip: width < minimumWidth ? text : "" | | |||
416 | onClicked: notificationItem.action(model.id) | | |||
417 | } | | |||
418 | } | | |||
419 | } | | |||
420 | } | | |||
421 | | ||||
422 | Loader { | | |||
423 | id: thumbnailStripLoader | | |||
424 | Layout.fillWidth: true | | |||
425 | Layout.preferredHeight: item ? item.implicitHeight : 0 | | |||
426 | source: "ThumbnailStrip.qml" | | |||
427 | active: notificationItem.urls.length > 0 | | |||
428 | } | 327 | } | ||
429 | } | 328 | } | ||
329 | ] | ||||
430 | } | 330 | } |