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