Kirigami2

ActionButton.qml
1 /*
2  * SPDX-FileCopyrightText: 2015 Marco Martin <[email protected]>
3  *
4  * SPDX-License-Identifier: LGPL-2.0-or-later
5  */
6 
7 import QtQuick 2.15
8 import QtQuick.Controls 2.0 as QQC2
9 import QtGraphicalEffects 1.0 as GE
10 import org.kde.kirigami 2.16 as Kirigami
11 
12 Item {
13  id: root
14 
15  anchors {
16  left: parent.left
17  right: parent.right
18  bottom: parent.bottom
19  bottomMargin: root.page.footer ? root.page.footer.height : 0
20  }
21  //smallSpacing for the shadow
22  implicitHeight: button.height + Kirigami.Units.smallSpacing
23  clip: true
24 
25  readonly property Kirigami.Page page: root.parent.page
26  //either Action or QAction should work here
27 
28  function isActionAvailable(action) { return action && (action.hasOwnProperty("visible") ? action.visible === undefined || action.visible : !action.hasOwnProperty("visible")); }
29 
30  readonly property QtObject action: root.page && isActionAvailable(root.page.mainAction) ? root.page.mainAction : null
31  readonly property QtObject leftAction: root.page && isActionAvailable(root.page.leftAction) ? root.page.leftAction : null
32  readonly property QtObject rightAction: root.page && isActionAvailable(root.page.rightAction) ? root.page.rightAction : null
33 
34  readonly property bool hasApplicationWindow: typeof applicationWindow !== "undefined" && applicationWindow
35  readonly property bool hasGlobalDrawer: typeof globalDrawer !== "undefined" && globalDrawer
36  readonly property bool hasContextDrawer: typeof contextDrawer !== "undefined" && contextDrawer
37 
38  transform: Translate {
39  id: translateTransform
40  }
41 
42  states: [
43  State {
44  when: mouseArea.internalVisibility
45  PropertyChanges {
46  target: translateTransform
47  y: 0
48  }
49  PropertyChanges {
50  target: root
51  opacity: 1
52  }
53  PropertyChanges {
54  target: root
55  visible: true
56  }
57  },
58  State {
59  when: !mouseArea.internalVisibility
60  PropertyChanges {
61  target: translateTransform
62  y: button.height
63  }
64  PropertyChanges {
65  target: root
66  opacity: 0
67  }
68  PropertyChanges {
69  target: root
70  visible: false
71  }
72  }
73  ]
74  transitions: Transition {
75  ParallelAnimation {
76  NumberAnimation {
77  target: translateTransform
78  property: "y"
79  duration: Kirigami.Units.longDuration
80  easing.type: mouseArea.internalVisibility ? Easing.InQuad : Easing.OutQuad
81  }
82  OpacityAnimator {
83  duration: Kirigami.Units.longDuration
84  easing.type: Easing.InOutQuad
85  }
86  }
87  }
88 
89  onWidthChanged: button.x = Qt.binding(() => (root.width / 2 - button.width / 2))
90  Item {
91  id: button
92  x: root.width/2 - button.width/2
93 
94  property int mediumIconSizing: Kirigami.Units.iconSizes.medium
95  property int largeIconSizing: Kirigami.Units.iconSizes.large
96 
97  anchors.bottom: edgeMouseArea.bottom
98 
99  implicitWidth: implicitHeight + mediumIconSizing*2 + Kirigami.Units.gridUnit
100  implicitHeight: largeIconSizing + Kirigami.Units.largeSpacing*2
101 
102 
103  onXChanged: {
104  if (mouseArea.pressed || edgeMouseArea.pressed || fakeContextMenuButton.pressed) {
105  if (root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal) {
106  globalDrawer.peeking = true;
107  globalDrawer.visible = true;
108  if (Qt.application.layoutDirection === Qt.LeftToRight) {
109  globalDrawer.position = Math.min(1, Math.max(0, (x - root.width/2 + button.width/2)/globalDrawer.contentItem.width + mouseArea.drawerShowAdjust));
110  } else {
111  globalDrawer.position = Math.min(1, Math.max(0, (root.width/2 - button.width/2 - x)/globalDrawer.contentItem.width + mouseArea.drawerShowAdjust));
112  }
113  }
114  if (root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal) {
115  contextDrawer.peeking = true;
116  contextDrawer.visible = true;
117  if (Qt.application.layoutDirection === Qt.LeftToRight) {
118  contextDrawer.position = Math.min(1, Math.max(0, (root.width/2 - button.width/2 - x)/contextDrawer.contentItem.width + mouseArea.drawerShowAdjust));
119  } else {
120  contextDrawer.position = Math.min(1, Math.max(0, (x - root.width/2 + button.width/2)/contextDrawer.contentItem.width + mouseArea.drawerShowAdjust));
121  }
122  }
123  }
124  }
125 
126  MouseArea {
127  id: mouseArea
128  anchors.fill: parent
129 
130  visible: action !== null || leftAction !== null || rightAction !== null
131  property bool internalVisibility: (!root.hasApplicationWindow || (applicationWindow().controlsVisible && applicationWindow().height > root.height*2)) && (root.action === null || root.action.visible === undefined || root.action.visible)
132  preventStealing: true
133 
134  drag {
135  target: button
136  //filterChildren: true
137  axis: Drag.XAxis
138  minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2
139  maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2
140  }
141 
142  property var downTimestamp;
143  property int startX
144  property int startMouseY
145  property real drawerShowAdjust
146 
147  readonly property int currentThird: (3*mouseX)/width
148  readonly property QtObject actionUnderMouse: {
149  switch(currentThird) {
150  case 0: return leftAction;
151  case 1: return action;
152  case 2: return rightAction;
153  default: return null
154  }
155  }
156 
157  hoverEnabled: true
158 
159  QQC2.ToolTip.visible: containsMouse && !Kirigami.Settings.tabletMode && actionUnderMouse
160  QQC2.ToolTip.text: actionUnderMouse ? actionUnderMouse.text : ""
161  QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
162 
163  onPressed: mouse => {
164  // search if we have a page to set to current
165  if (root.hasApplicationWindow && applicationWindow().pageStack.currentIndex !== undefined && root.page.Kirigami.ColumnView.level !== undefined) {
166  // search the button parent's parent, that is the page parent
167  // this will make the context drawer open for the proper page
168  applicationWindow().pageStack.currentIndex = root.page.Kirigami.ColumnView.level;
169  }
170  downTimestamp = (new Date()).getTime();
171  startX = button.x + button.width/2;
172  startMouseY = mouse.y;
173  drawerShowAdjust = 0;
174  }
175  onReleased: mouse => {
176  tooltipHider.restart();
177  if (root.hasGlobalDrawer) globalDrawer.peeking = false;
178  if (root.hasContextDrawer) contextDrawer.peeking = false;
179  // pixel/second
180  const x = button.x + button.width/2;
181  const speed = ((x - startX) / ((new Date()).getTime() - downTimestamp) * 1000);
182  drawerShowAdjust = 0;
183 
184  // project where it would be a full second in the future
185  if (root.hasContextDrawer && root.hasGlobalDrawer && globalDrawer.modal && x + speed > Math.min(root.width/4*3, root.width/2 + globalDrawer.contentItem.width/2)) {
186  globalDrawer.open();
187  contextDrawer.close();
188  } else if (root.hasContextDrawer && x + speed < Math.max(root.width/4, root.width/2 - contextDrawer.contentItem.width/2)) {
189  if (root.hasContextDrawer && contextDrawer.modal) {
190  contextDrawer.open();
191  }
192  if (root.hasGlobalDrawer && globalDrawer.modal) {
193  globalDrawer.close();
194  }
195  } else {
196  if (root.hasGlobalDrawer && globalDrawer.modal) {
197  globalDrawer.close();
198  }
199  if (root.hasContextDrawer && contextDrawer.modal) {
200  contextDrawer.close();
201  }
202  }
203  // Don't rely on native onClicked, but fake it here:
204  // Qt.startDragDistance is not adapted to devices dpi in case
205  // of Android, so consider the button "clicked" when:
206  // *the button has been dragged less than a gridunit
207  // *the finger is still on the button
208  if (Math.abs((button.x + button.width/2) - startX) < Kirigami.Units.gridUnit &&
209  mouse.y > 0) {
210 
211  //if an action has been assigned, trigger it
212  if (actionUnderMouse && actionUnderMouse.trigger) {
213  actionUnderMouse.trigger();
214  }
215 
216  if (actionUnderMouse && actionUnderMouse.hasOwnProperty("children") && actionUnderMouse.children.length > 0) {
217  let subMenuUnderMouse;
218  switch (actionUnderMouse) {
219  case leftAction:
220  subMenuUnderMouse = leftActionSubMenu;
221  break;
222  case mainAction:
223  subMenuUnderMouse = mainActionSubMenu;
224  break
225  case rightAction:
226  subMenuUnderMouse = rightActionSubMenu;
227  break;
228  }
229  if (subMenuUnderMouse && !subMenuUnderMouse.visible) {
230  subMenuUnderMouse.visible = true;
231  }
232  }
233  }
234  }
235 
236  onPositionChanged: mouse => {
237  drawerShowAdjust = Math.min(0.3, Math.max(0, (startMouseY - mouse.y)/(Kirigami.Units.gridUnit*15)));
238  button.xChanged();
239  }
240  onPressAndHold: mouse => {
241  if (!actionUnderMouse) {
242  return;
243  }
244 
245  // if an action has been assigned, show a message like a tooltip
246  if (actionUnderMouse && actionUnderMouse.text && Kirigami.Settings.tabletMode) {
247  tooltipHider.stop();
248  QQC2.ToolTip.show(actionUnderMouse.text);
249  // The tooltip is shown perpetually while we are pressed and held, and
250  // we start tooltipHider below when the press is released. This ensures
251  // that the user can have as much time as they want to read the tooltip,
252  // and also that the tooltip is hidden in a pleasant manner that does
253  // not feel overly urgent.
254  }
255  }
256  Timer {
257  id: tooltipHider
258  interval: Kirigami.Units.humanMoment
259  onTriggered: {
260  QQC2.ToolTip.hide();
261  }
262  }
263  Connections {
264  target: root.hasGlobalDrawer ? globalDrawer : null
265  function onPositionChanged() {
266  if ( globalDrawer && globalDrawer.modal && !mouseArea.pressed && !edgeMouseArea.pressed && !fakeContextMenuButton.pressed) {
267  if (Qt.application.layoutDirection === Qt.LeftToRight) {
268  button.x = globalDrawer.contentItem.width * globalDrawer.position + root.width/2 - button.width/2;
269  } else {
270  button.x = -globalDrawer.contentItem.width * globalDrawer.position + root.width/2 - button.width/2
271  }
272  }
273  }
274  }
275  Connections {
276  target: root.hasContextDrawer ? contextDrawer : null
277  function onPositionChanged() {
278  if (contextDrawer && contextDrawer.modal && !mouseArea.pressed && !edgeMouseArea.pressed && !fakeContextMenuButton.pressed) {
279  if (Qt.application.layoutDirection === Qt.LeftToRight) {
280  button.x = root.width/2 - button.width/2 - contextDrawer.contentItem.width * contextDrawer.position;
281  } else {
282  button.x = root.width/2 - button.width/2 + contextDrawer.contentItem.width * contextDrawer.position;
283  }
284  }
285  }
286  }
287 
288  Item {
289  id: background
290  anchors {
291  fill: parent
292  }
293 
294  Rectangle {
295  id: buttonGraphics
296  radius: width/2
297  anchors.centerIn: parent
298  height: parent.height - Kirigami.Units.smallSpacing*2
299  width: height
300  enabled: root.action && root.action.enabled
301  visible: root.action
302  readonly property bool pressed: root.action && root.action.enabled && ((root.action === mouseArea.actionUnderMouse && mouseArea.pressed) || root.action.checked)
303  property color baseColor: root.action && root.action.icon && root.action.icon.color && root.action.icon.color !== undefined && root.action.icon.color.a > 0 ? root.action.icon.color : Kirigami.Theme.highlightColor
304  color: pressed ? Qt.darker(baseColor, 1.3) : baseColor
305 
306  ActionsMenu {
307  id: mainActionSubMenu
308  y: -height
309  x: -width/2 + parent.width/2
310  actions: root.action && root.action.hasOwnProperty("children") ? root.action.children : ""
311  submenuComponent: Component {
312  ActionsMenu {}
313  }
314  }
315  Kirigami.Icon {
316  id: icon
317  anchors.centerIn: parent
318  width: button.mediumIconSizing
319  height: width
320  source: root.action && root.action.icon.name ? root.action.icon.name : ""
321  selected: true
322  color: root.action && root.action.icon && root.action.icon.color && root.action.icon.color.a > 0 ? root.action.icon.color : (selected ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor)
323  }
324  Behavior on color {
325  ColorAnimation {
326  duration: Kirigami.Units.shortDuration
327  easing.type: Easing.InOutQuad
328  }
329  }
330  Behavior on x {
331  NumberAnimation {
332  duration: Kirigami.Units.longDuration
333  easing.type: Easing.InOutQuad
334  }
335  }
336  }
337  // left button
338  Rectangle {
339  id: leftButtonGraphics
340  z: -1
341  anchors {
342  left: parent.left
343  bottom: parent.bottom
344  bottomMargin: Kirigami.Units.smallSpacing
345  }
346  enabled: root.leftAction && root.leftAction.enabled
347  radius: 2
348  height: button.mediumIconSizing + Kirigami.Units.smallSpacing * 2
349  width: height + (root.action ? Kirigami.Units.gridUnit*2 : 0)
350  visible: root.leftAction
351 
352  readonly property bool pressed: root.leftAction && root.leftAction.enabled && ((mouseArea.actionUnderMouse === root.leftAction && mouseArea.pressed) || root.leftAction.checked)
353  property color baseColor: root.leftAction && root.leftAction.icon && root.leftAction.icon.color && root.leftAction.icon.color !== undefined && root.leftAction.icon.color.a > 0 ? root.leftAction.icon.color : Kirigami.Theme.highlightColor
354  color: pressed ? baseColor : Kirigami.Theme.backgroundColor
355  Behavior on color {
356  ColorAnimation {
357  duration: Kirigami.Units.shortDuration
358  easing.type: Easing.InOutQuad
359  }
360  }
361  ActionsMenu {
362  id: leftActionSubMenu
363  y: -height
364  x: -width/2 + parent.width/2
365  actions: root.leftAction && root.leftAction.hasOwnProperty("children") ? root.leftAction.children : ""
366  submenuComponent: Component {
367  ActionsMenu {}
368  }
369  }
370  Kirigami.Icon {
371  source: root.leftAction && root.leftAction.icon.name ? root.leftAction.icon.name : ""
372  width: button.mediumIconSizing
373  height: width
374  selected: leftButtonGraphics.pressed
375  color: root.leftAction && root.leftAction.icon && root.leftAction.icon.color && root.leftAction.icon.color.a > 0 ? root.leftAction.icon.color : (selected ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor)
376  anchors {
377  left: parent.left
378  verticalCenter: parent.verticalCenter
379  margins: root.action ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.smallSpacing
380  }
381  }
382  }
383  //right button
384  Rectangle {
385  id: rightButtonGraphics
386  z: -1
387  anchors {
388  right: parent.right
389  // verticalCenter: parent.verticalCenter
390  bottom: parent.bottom
391  bottomMargin: Kirigami.Units.smallSpacing
392  }
393  enabled: root.rightAction && root.rightAction.enabled
394  radius: 2
395  height: button.mediumIconSizing + Kirigami.Units.smallSpacing * 2
396  width: height + (root.action ? Kirigami.Units.gridUnit*2 : 0)
397  visible: root.rightAction
398  readonly property bool pressed: root.rightAction && root.rightAction.enabled && ((mouseArea.actionUnderMouse === root.rightAction && mouseArea.pressed) || root.rightAction.checked)
399  property color baseColor: root.rightAction && root.rightAction.icon && root.rightAction.icon.color && root.rightAction.icon.color !== undefined && root.rightAction.icon.color.a > 0 ? root.rightAction.icon.color : Kirigami.Theme.highlightColor
400  color: pressed ? baseColor : Kirigami.Theme.backgroundColor
401  Behavior on color {
402  ColorAnimation {
403  duration: Kirigami.Units.shortDuration
404  easing.type: Easing.InOutQuad
405  }
406  }
407  ActionsMenu {
408  id: rightActionSubMenu
409  y: -height
410  x: -width/2 + parent.width/2
411  actions: root.rightAction && root.rightAction.hasOwnProperty("children") ? root.rightAction.children : ""
412  submenuComponent: Component {
413  ActionsMenu {}
414  }
415  }
416  Kirigami.Icon {
417  source: root.rightAction && root.rightAction.icon.name ? root.rightAction.icon.name : ""
418  width: button.mediumIconSizing
419  height: width
420  selected: rightButtonGraphics.pressed
421  color: root.rightAction && root.rightAction.icon && root.rightAction.icon.color && root.rightAction.icon.color.a > 0 ? root.rightAction.icon.color : (selected ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor)
422  anchors {
423  right: parent.right
424  verticalCenter: parent.verticalCenter
425  margins: root.action ? Kirigami.Units.smallSpacing * 2 : Kirigami.Units.smallSpacing
426  }
427  }
428  }
429  }
430 
431  GE.DropShadow {
432  anchors.fill: background
433  horizontalOffset: 0
434  verticalOffset: 1
435  radius: Kirigami.Units.gridUnit /2
436  samples: 16
437  color: Qt.rgba(0, 0, 0, mouseArea.pressed ? 0.6 : 0.4)
438  source: background
439  }
440  }
441  }
442 
443  MouseArea {
444  id: fakeContextMenuButton
445  anchors {
446  right: edgeMouseArea.right
447  bottom: parent.bottom
448  margins: Kirigami.Units.smallSpacing
449  }
450  drag {
451  target: button
452  axis: Drag.XAxis
453  minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2
454  maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2
455  }
456  visible: root.page.actions && root.page.actions.contextualActions.length > 0 && ((typeof applicationWindow === "undefined") || applicationWindow().wideScreen)
457  // using internal pagerow api
458  && ((typeof applicationWindow !== "undefined") && root.page && root.page.parent ? root.page.Kirigami.ColumnView.level < applicationWindow().pageStack.depth-1 : (typeof applicationWindow === "undefined"))
459 
460  width: button.mediumIconSizing + Kirigami.Units.smallSpacing*2
461  height: width
462 
463 
464  GE.DropShadow {
465  anchors.fill: handleGraphics
466  horizontalOffset: 0
467  verticalOffset: 1
468  radius: Kirigami.Units.gridUnit /2
469  samples: 16
470  color: Qt.rgba(0, 0, 0, fakeContextMenuButton.pressed ? 0.6 : 0.4)
471  source: handleGraphics
472  }
473  Rectangle {
474  id: handleGraphics
475  anchors.fill: parent
476  color: fakeContextMenuButton.pressed ? Kirigami.Theme.highlightColor : Kirigami.Theme.backgroundColor
477  radius: 1
478  Kirigami.Icon {
479  anchors.centerIn: parent
480  width: button.mediumIconSizing
481  selected: fakeContextMenuButton.pressed
482  height: width
483  source: "overflow-menu"
484  }
485  Behavior on color {
486  ColorAnimation {
487  duration: Kirigami.Units.shortDuration
488  easing.type: Easing.InOutQuad
489  }
490  }
491  }
492 
493  onPressed: mouse => {
494  mouseArea.onPressed(mouse)
495  }
496  onReleased: mouse => {
497  const pos = root.mapFromItem(fakeContextMenuButton, mouse.x, mouse.y);
498 
499  if ((typeof contextDrawer !== "undefined") && contextDrawer) {
500  contextDrawer.peeking = false;
501 
502  if (pos.x < root.width/2) {
503  contextDrawer.open();
504  } else if (contextDrawer.drawerOpen && mouse.x > 0 && mouse.x < width) {
505  contextDrawer.close();
506  }
507  }
508 
509  if ((typeof globalDrawer !== "undefined") && globalDrawer) {
510  globalDrawer.peeking = false;
511 
512  if (globalDrawer.position > 0.5) {
513  globalDrawer.open();
514  } else {
515  globalDrawer.close();
516  }
517  }
518  if (containsMouse && ((typeof globalDrawer === "undefined") || !globalDrawer || !globalDrawer.drawerOpen || !globalDrawer.modal) &&
519  ((typeof contextDrawer === "undefined") || !contextDrawer || !contextDrawer.drawerOpen || !contextDrawer.modal)) {
520  contextMenu.visible = !contextMenu.visible;
521  }
522  }
523  ActionsMenu {
524  id: contextMenu
525  x: parent.width - width
526  y: -height
527  actions: root.page.actions.contextualActions
528  submenuComponent: Component {
529  ActionsMenu {}
530  }
531  }
532  }
533 
534  MouseArea {
535  id: edgeMouseArea
536  z:99
537  anchors {
538  left: parent.left
539  right: parent.right
540  bottom: parent.bottom
541  }
542  drag {
543  target: button
544  //filterChildren: true
545  axis: Drag.XAxis
546  minimumX: root.hasContextDrawer && contextDrawer.enabled && contextDrawer.modal ? 0 : root.width/2 - button.width/2
547  maximumX: root.hasGlobalDrawer && globalDrawer.enabled && globalDrawer.modal ? root.width : root.width/2 - button.width/2
548  }
549  height: Kirigami.Units.smallSpacing * 3
550 
551  onPressed: mouse => mouseArea.onPressed(mouse)
552  onPositionChanged: mouse => mouseArea.positionChanged(mouse)
553  onReleased: mouse => mouseArea.released(mouse)
554  }
555 }
KDOCTOOLS_EXPORT QString transform(const QString &file, const QString &stylesheet, const QVector< const char * > &params=QVector< const char * >())
QTextStream & right(QTextStream &stream)
QTextStream & left(QTextStream &stream)
QTextStream & left(QTextStream &s)
QTextStream & right(QTextStream &s)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Feb 7 2023 04:14:23 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.