Unity 8
MenuPopup.qml
1 /*
2  * Copyright 2016 Canonical Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.4
18 import QtQuick.Layouts 1.1
19 import Ubuntu.Components 1.3
20 import Ubuntu.Components.ListItems 1.3 as ListItems
21 import "../Components"
22 import "../Components/PanelState"
23 import "."
24 
25 UbuntuShape {
26  id: root
27  objectName: "menu"
28  backgroundColor: theme.palette.normal.overlay
29 
30  signal childActivated()
31 
32  // true for submenus that need to show on the other side of their parent
33  // if they don't fit when growing right
34  property bool substractWidth: false
35 
36  property bool selectFirstOnCountChange: true
37 
38  property real desiredX
39  x: {
40  var dummy = visible; // force recalc when shown/hidden
41  var parentTopLeft = parent.mapToItem(null, 0, 0);
42  var farX = ApplicationMenusLimits.screenWidth;
43  if (parentTopLeft.x + width + desiredX <= farX) {
44  return desiredX;
45  } else {
46  if (substractWidth) {
47  return -width;
48  } else {
49  return farX - parentTopLeft.x - width;
50  }
51  }
52  }
53 
54  property real desiredY
55  y: {
56  var dummy = visible; // force recalc when shown/hidden
57  var parentTopLeft = parent.mapToItem(null, 0, 0);
58  var bottomY = ApplicationMenusLimits.screenHeight;
59  if (parentTopLeft.y + height + desiredY <= bottomY) {
60  return desiredY;
61  } else {
62  return bottomY - parentTopLeft.y - height;
63  }
64  }
65 
66  property alias unityMenuModel: repeater.model
67  property PanelState panelState
68 
69  function show() {
70  visible = true;
71  focusScope.forceActiveFocus();
72  }
73 
74  function hide() {
75  visible = false;
76  d.currentItem = null;
77  }
78 
79  function selectFirstIndex() {
80  d.selectNext(-1);
81  }
82 
83  function reset() {
84  d.currentItem = null;
85  dismiss();
86  }
87 
88  function dismiss() {
89  d.dismissAll();
90  }
91 
92  implicitWidth: focusScope.width
93  implicitHeight: focusScope.height
94 
95  MenuNavigator {
96  id: d
97  objectName: "d"
98  itemView: repeater
99 
100  property Item currentItem: null
101  property Item hoveredItem: null
102  readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
103 
104  property real __minimumWidth: units.gu(20)
105  property real __maximumWidth: ApplicationMenusLimits.screenWidth * 0.7
106  property real __minimumHeight: units.gu(2)
107  property real __maximumHeight: ApplicationMenusLimits.screenHeight - panelState.panelHeight
108 
109  signal dismissAll()
110 
111  onCurrentItemChanged: {
112  if (currentItem) {
113  currentItem.item.forceActiveFocus();
114  } else {
115  hoveredItem = null;
116  }
117 
118  submenuHoverTimer.stop();
119  }
120 
121  onSelect: {
122  currentItem = repeater.itemAt(index);
123  if (currentItem) {
124  if (currentItem.y < listView.contentY) {
125  listView.contentY = currentItem.y;
126  } else if (currentItem.y + currentItem.height > listView.contentY + listView.height) {
127  listView.contentY = currentItem.y + currentItem.height - listView.height;
128  }
129  }
130  }
131  }
132 
133  MouseArea {
134  // Eat events.
135  anchors.fill: parent
136  }
137 
138  Item {
139  id: focusScope
140  width: container.width
141  height: container.height
142  focus: visible
143 
144  Keys.onUpPressed: d.selectPrevious(d.currentIndex)
145  Keys.onDownPressed: d.selectNext(d.currentIndex)
146  Keys.onRightPressed: {
147  // Don't let right keypresses fall through if the current item has a visible popup.
148  if (!d.currentItem || !d.currentItem.popup || !d.currentItem.popup.visible) {
149  event.accepted = false;
150  }
151  }
152 
153  ColumnLayout {
154  id: container
155  objectName: "container"
156 
157  height: MathUtils.clamp(listView.contentHeight, d.__minimumHeight, d.__maximumHeight)
158  width: menuColumn.width
159  spacing: 0
160 
161  // Header - scroll up
162  Item {
163  Layout.fillWidth: true
164  height: units.gu(3)
165  visible: listView.contentHeight > root.height
166  enabled: !listView.atYBeginning
167  z: 1
168 
169  Rectangle {
170  color: enabled ? theme.palette.normal.overlayText :
171  theme.palette.disabled.overlayText
172  height: units.dp(1)
173  anchors {
174  bottom: parent.bottom
175  left: parent.left
176  right: parent.right
177  }
178  }
179 
180  Icon {
181  anchors.centerIn: parent
182  width: units.gu(2)
183  height: units.gu(2)
184  name: "up"
185  color: enabled ? theme.palette.normal.overlayText :
186  theme.palette.disabled.overlayText
187  }
188 
189  MouseArea {
190  id: previousMA
191  anchors.fill: parent
192  hoverEnabled: enabled
193  onPressed: progress()
194 
195  Timer {
196  running: previousMA.containsMouse && !listView.atYBeginning
197  interval: 1000
198  repeat: true
199  onTriggered: previousMA.progress()
200  }
201 
202  function progress() {
203  var item = menuColumn.childAt(0, listView.contentY);
204  if (item) {
205  var previousItem = item;
206  do {
207  previousItem = repeater.itemAt(previousItem.__ownIndex-1);
208  if (!previousItem) {
209  listView.contentY = 0;
210  return;
211  }
212  } while (previousItem.__isSeparator);
213 
214  listView.contentY = previousItem.y
215  }
216  }
217  }
218  }
219 
220  // Menu Items
221  Flickable {
222  id: listView
223  clip: interactive
224 
225  Layout.fillHeight: true
226  Layout.fillWidth: true
227  contentHeight: menuColumn.height
228  interactive: height < contentHeight
229 
230  Timer {
231  id: submenuHoverTimer
232  interval: 225 // GTK MENU_POPUP_DELAY, Qt SH_Menu_SubMenuPopupDelay in QCommonStyle is 256
233  onTriggered: d.currentItem.item.trigger();
234  }
235 
236  MouseArea {
237  anchors.fill: parent
238  hoverEnabled: true
239  z: 1 // on top so we override any other hovers
240  onEntered: updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
241  onPositionChanged: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
242 
243  function updateCurrentItemFromPosition(point) {
244  var pos = mapToItem(listView.contentItem, point.x, point.y);
245 
246  if (!d.hoveredItem || !d.currentItem ||
247  !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
248  submenuHoverTimer.stop();
249 
250  d.hoveredItem = menuColumn.childAt(pos.x, pos.y)
251  if (!d.hoveredItem || !d.hoveredItem.enabled)
252  return;
253  d.currentItem = d.hoveredItem;
254 
255  if (!d.currentItem.__isSeparator && d.currentItem.item.hasSubmenu && d.currentItem.item.enabled) {
256  submenuHoverTimer.start();
257  }
258  }
259  }
260 
261  onClicked: {
262  var pos = mapToItem(listView.contentItem, mouse.x, mouse.y);
263  var clickedItem = menuColumn.childAt(pos.x, pos.y);
264  if (clickedItem.enabled && !clickedItem.__isSeparator) {
265  clickedItem.item.trigger();
266  }
267  }
268  }
269 
270  ActionContext {
271  id: menuBarContext
272  objectName: "menuContext"
273  active: {
274  if (!root.visible) return false;
275  if (d.currentItem && d.currentItem.popup && d.currentItem.popup.visible) {
276  return false;
277  }
278  return true;
279  }
280  }
281 
282  Component {
283  id: separatorComponent
284  ListItems.ThinDivider {
285  // Parent will be loader
286  objectName: parent.objectName + "-separator"
287  implicitHeight: units.dp(2)
288  }
289  }
290 
291  Component {
292  id: menuItemComponent
293  MenuItem {
294  // Parent will be loader
295  id: menuItem
296  menuData: parent.__menuData
297  objectName: parent.objectName + "-actionItem"
298 
299  width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
300 
301  property Item popup: null
302 
303  action.onTriggered: {
304  submenuHoverTimer.stop();
305 
306  d.currentItem = parent;
307 
308  if (hasSubmenu) {
309  if (!popup) {
310  root.unityMenuModel.aboutToShow(__ownIndex);
311  var model = root.unityMenuModel.submenu(__ownIndex);
312  popup = submenuComponent.createObject(focusScope, {
313  objectName: parent.objectName + "-",
314  unityMenuModel: model,
315  substractWidth: true,
316  desiredX: Qt.binding(function() { return root.width }),
317  desiredY: Qt.binding(function() {
318  var dummy = listView.contentY; // force a recalc on contentY change.
319  return mapToItem(container, 0, y).y;
320  })
321  });
322  popup.retreat.connect(function() {
323  popup.destroy();
324  popup = null;
325  menuItem.forceActiveFocus();
326  });
327  popup.childActivated.connect(function() {
328  popup.destroy();
329  popup = null;
330  root.childActivated();
331  });
332  } else if (!popup.visible) {
333  root.unityMenuModel.aboutToShow(__ownIndex);
334  popup.visible = true;
335  popup.item.selectFirstIndex();
336  }
337  } else {
338  root.unityMenuModel.activate(__ownIndex);
339  root.childActivated();
340  }
341  }
342 
343  Connections {
344  target: d
345  onCurrentIndexChanged: {
346  if (popup && d.currentIndex != __ownIndex) {
347  popup.visible = false;
348  }
349  }
350  onDismissAll: {
351  if (popup) {
352  popup.destroy();
353  popup = null;
354  }
355  }
356  }
357 
358  Component.onDestruction: {
359  if (popup) {
360  popup.destroy();
361  popup = null;
362  }
363  }
364  }
365  }
366 
367  ColumnLayout {
368  id: menuColumn
369  spacing: 0
370 
371  width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
372 
373  Repeater {
374  id: repeater
375 
376  onCountChanged: {
377  if (root.selectFirstOnCountChange && !d.currentItem && count > 0) {
378  root.selectFirstIndex();
379  }
380  }
381 
382  Loader {
383  id: loader
384  objectName: root.objectName + "-item" + __ownIndex
385 
386  readonly property var popup: item ? item.popup : null
387  property var __menuData: model
388  property int __ownIndex: index
389  property bool __isSeparator: model.isSeparator
390 
391  enabled: __isSeparator ? false : model.sensitive
392 
393  sourceComponent: {
394  if (model.isSeparator) {
395  return separatorComponent;
396  }
397  return menuItemComponent;
398  }
399 
400  Layout.fillWidth: true
401  }
402 
403  }
404  }
405 
406  // Highlight
407  Rectangle {
408  color: "transparent"
409  border.width: units.dp(1)
410  border.color: UbuntuColors.orange
411  z: 1
412 
413  width: listView.width
414  height: d.currentItem ? d.currentItem.height : 0
415  y: d.currentItem ? d.currentItem.y : 0
416  visible: d.currentItem
417  }
418 
419  } // Flickable
420 
421  // Header - scroll down
422  Item {
423  Layout.fillWidth: true
424  height: units.gu(3)
425  visible: listView.contentHeight > root.height
426  enabled: !listView.atYEnd
427  z: 1
428 
429  Rectangle {
430  color: enabled ? theme.palette.normal.overlayText :
431  theme.palette.disabled.overlayText
432  height: units.dp(1)
433  anchors {
434  top: parent.top
435  left: parent.left
436  right: parent.right
437  }
438  }
439 
440  Icon {
441  anchors.centerIn: parent
442  width: units.gu(2)
443  height: units.gu(2)
444  name: "down"
445  color: enabled ? theme.palette.normal.overlayText :
446  theme.palette.disabled.overlayText
447  }
448 
449  MouseArea {
450  id: nextMA
451  anchors.fill: parent
452  hoverEnabled: enabled
453  onPressed: progress()
454 
455  Timer {
456  running: nextMA.containsMouse && !listView.atYEnd
457  interval: 1000
458  repeat: true
459  onTriggered: nextMA.progress()
460  }
461 
462  function progress() {
463  var item = menuColumn.childAt(0, listView.contentY + listView.height);
464  if (item) {
465  var nextItem = item;
466  do {
467  nextItem = repeater.itemAt(nextItem.__ownIndex+1);
468  if (!nextItem) {
469  listView.contentY = listView.contentHeight - listView.height;
470  return;
471  }
472  } while (nextItem.__isSeparator);
473 
474  listView.contentY = nextItem.y - listView.height
475  }
476  }
477  }
478  }
479  } // Column
480 
481  Component {
482  id: submenuComponent
483  Loader {
484  id: submenuLoader
485  source: "MenuPopup.qml"
486 
487  property real desiredX
488  property real desiredY
489  property bool substractWidth
490  property var unityMenuModel: null
491  signal retreat()
492  signal childActivated()
493 
494  onLoaded: {
495  item.unityMenuModel = Qt.binding(function() { return submenuLoader.unityMenuModel; });
496  item.panelState = Qt.binding(function() { return root.panelState; });
497  item.objectName = Qt.binding(function() { return submenuLoader.objectName + "menu"; });
498  item.desiredX = Qt.binding(function() { return submenuLoader.desiredX; });
499  item.desiredY = Qt.binding(function() { return submenuLoader.desiredY; });
500  item.substractWidth = Qt.binding(function() { return submenuLoader.substractWidth; });
501  }
502 
503  Keys.onLeftPressed: retreat()
504 
505  Connections {
506  target: item
507  onChildActivated: childActivated();
508  }
509  }
510  }
511  }
512 }