Unity 8
MenuBar.qml
1 /*
2  * Copyright 2016, 2017 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 Utils 0.1
20 import Ubuntu.Components 1.3
21 import GlobalShortcut 1.0
22 
23 Item {
24  id: root
25  objectName: "menuBar"
26 
27  // set from outside
28  property alias unityMenuModel: rowRepeater.model
29  property bool enableKeyFilter: false
30  property real overflowWidth: width
31  property bool windowMoving: false
32 
33  // read from outside
34  readonly property bool valid: rowRepeater.count > 0
35  readonly property bool showRequested: d.longAltPressed || d.currentItem != null
36 
37  // MoveHandler API for DecoratedWindow
38  signal pressed(var mouse)
39  signal pressedChangedEx(bool pressed, var pressedButtons, real mouseX, real mouseY)
40  signal positionChanged(var mouse)
41  signal released(var mouse)
42  signal doubleClicked(var mouse)
43 
44  implicitWidth: row.width
45  height: parent.height
46 
47  function dismiss() {
48  d.dismissAll();
49  }
50 
51  function invokeMenu(mouseEvent) {
52  mouseArea.onClicked(mouseEvent);
53  }
54 
55  GlobalShortcut {
56  shortcut: Qt.Key_Alt|Qt.AltModifier
57  active: enableKeyFilter
58  onTriggered: d.startShortcutTimer()
59  onReleased: d.stopSHortcutTimer()
60  }
61  // On an actual keyboard, the AltModifier is not supplied on release.
62  GlobalShortcut {
63  shortcut: Qt.Key_Alt
64  active: enableKeyFilter
65  onTriggered: d.startShortcutTimer()
66  onReleased: d.stopSHortcutTimer()
67  }
68 
69  GlobalShortcut {
70  shortcut: Qt.AltModifier | Qt.Key_F10
71  active: enableKeyFilter && d.currentItem == null
72  onTriggered: {
73  for (var i = 0; i < rowRepeater.count; i++) {
74  var item = rowRepeater.itemAt(i);
75  if (item.enabled) {
76  item.show();
77  break;
78  }
79  }
80  }
81  }
82 
83  InverseMouseArea {
84  acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
85  anchors.fill: parent
86  enabled: d.currentItem != null
87  hoverEnabled: enabled && d.currentItem && d.currentItem.__popup != null
88  onPressed: { mouse.accepted = false; d.dismissAll(); }
89  }
90 
91  Row {
92  id: row
93  spacing: 0
94  height: parent.height
95 
96  ActionContext {
97  id: menuBarContext
98  objectName: "barContext"
99  active: !d.currentItem && enableKeyFilter
100  }
101 
102  Connections {
103  target: root.unityMenuModel
104  onModelReset: d.firstInvisibleIndex = undefined
105  }
106 
107  Component {
108  id: menuComponent
109  MenuPopup { }
110  }
111 
112  Repeater {
113  id: rowRepeater
114 
115  onItemAdded: d.recalcFirstInvisibleIndexAdded(index, item)
116  onCountChanged: d.recalcFirstInvisibleIndex()
117 
118  Item {
119  id: visualItem
120  objectName: root.objectName + "-item" + __ownIndex
121 
122  readonly property int __ownIndex: index
123  property Item __popup: null;
124  readonly property bool popupVisible: __popup && __popup.visible
125  readonly property bool shouldDisplay: x + width + ((__ownIndex < rowRepeater.count-1) ? units.gu(2) : 0) <
126  root.overflowWidth - ((__ownIndex < rowRepeater.count-1) ? overflowButton.width : 0)
127 
128  // First item is not centered, it has 0 gu on the left and 1 on the right
129  // so needs different width and anchors
130  readonly property bool isFirstItem: __ownIndex == 0
131 
132  implicitWidth: column.implicitWidth + (isFirstItem ? units.gu(1) : units.gu(2))
133  implicitHeight: row.height
134  enabled: (model.sensitive === true) && shouldDisplay
135  opacity: shouldDisplay ? 1 : 0
136 
137  function show() {
138  if (!__popup) {
139  root.unityMenuModel.aboutToShow(visualItem.__ownIndex);
140  __popup = menuComponent.createObject(root,
141  {
142  objectName: visualItem.objectName + "-menu",
143  desiredX: Qt.binding(function() { return visualItem.x - units.gu(1); }),
144  desiredY: Qt.binding(function() { return root.height; }),
145  unityMenuModel: Qt.binding(function() { return root.unityMenuModel.submenu(visualItem.__ownIndex); }),
146  selectFirstOnCountChange: false
147  });
148  __popup.reset();
149  __popup.childActivated.connect(dismiss);
150  // force the current item to be the newly popped up menu
151  } else if (!__popup.visible) {
152  root.unityMenuModel.aboutToShow(visualItem.__ownIndex);
153  __popup.show();
154  }
155  d.currentItem = visualItem;
156  }
157  function hide() {
158  if (__popup) {
159  __popup.hide();
160 
161  if (d.currentItem === visualItem) {
162  d.currentItem = null;
163  }
164  }
165  }
166  function dismiss() {
167  if (__popup) {
168  __popup.destroy();
169  __popup = null;
170 
171  if (d.currentItem === visualItem) {
172  d.currentItem = null;
173  }
174  }
175  }
176 
177  onVisibleChanged: {
178  if (!visible && __popup) dismiss();
179  }
180 
181  onShouldDisplayChanged: {
182  if ((!shouldDisplay && d.firstInvisibleIndex == undefined) || __ownIndex <= d.firstInvisibleIndex) {
183  d.recalcFirstInvisibleIndex();
184  }
185  }
186 
187  Connections {
188  target: d
189  onDismissAll: visualItem.dismiss()
190  }
191 
192  RowLayout {
193  id: column
194  spacing: units.gu(1)
195  anchors {
196  verticalCenter: parent.verticalCenter
197  horizontalCenter: !visualItem.isFirstItem ? parent.horizontalCenter : undefined
198  left: visualItem.isFirstItem ? parent.left : undefined
199  }
200 
201  Icon {
202  Layout.preferredWidth: units.gu(2)
203  Layout.preferredHeight: units.gu(2)
204  Layout.alignment: Qt.AlignVCenter
205 
206  visible: model.icon || false
207  source: model.icon || ""
208  }
209 
210  ActionItem {
211  id: actionItem
212  width: _title.width
213  height: _title.height
214 
215  action: Action {
216  enabled: visualItem.enabled
217  // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
218  text: model.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
219 
220  onTriggered: {
221  visualItem.show();
222  }
223  }
224 
225  Label {
226  id: _title
227  text: actionItem.text
228  horizontalAlignment: Text.AlignLeft
229  color: enabled ? theme.palette.normal.backgroundText : theme.palette.disabled.backgroundText
230  }
231  }
232  }
233 
234  Component.onDestruction: {
235  if (__popup) {
236  __popup.destroy();
237  __popup = null;
238  }
239  }
240  } // Item ( delegate )
241  } // Repeater
242  } // Row
243 
244  MouseArea {
245  id: mouseArea
246  anchors.fill: parent
247  hoverEnabled: d.currentItem
248 
249  property bool moved: false
250 
251  onEntered: {
252  if (d.currentItem) {
253  updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
254  }
255  }
256 
257  onClicked: {
258  if (!moved) {
259  var prevItem = d.currentItem;
260  updateCurrentItemFromPosition(Qt.point(mouseX, mouseY));
261  if (prevItem && d.currentItem == prevItem) {
262  prevItem.hide();
263  }
264  }
265  moved = false;
266  }
267 
268  // for the MoveHandler
269  onPressed: root.pressed(mouse)
270  onPressedChanged: root.pressedChangedEx(pressed, pressedButtons, mouseX, mouseY)
271  onReleased: root.released(mouse)
272  onDoubleClicked: root.doubleClicked(mouse)
273 
274  Mouse.ignoreSynthesizedEvents: true
275  Mouse.onPositionChanged: {
276  root.positionChanged(mouse);
277  moved = root.windowMoving;
278  if (d.currentItem) {
279  updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
280  }
281  }
282 
283  function updateCurrentItemFromPosition(point) {
284  var pos = mapToItem(row, point.x, point.y);
285 
286  if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
287  d.hoveredItem = row.childAt(pos.x, pos.y);
288  if (!d.hoveredItem || !d.hoveredItem.enabled)
289  return;
290  if (d.currentItem != d.hoveredItem) {
291  d.currentItem = d.hoveredItem;
292  }
293  }
294  }
295  }
296 
297  MouseArea {
298  id: overflowButton
299  objectName: "overflow"
300 
301  hoverEnabled: d.currentItem
302  onEntered: d.currentItem = this
303  onPositionChanged: d.currentItem = this
304  onPressed: d.currentItem = this
305 
306  property Item __popup: null;
307  readonly property bool popupVisible: __popup && __popup.visible
308  readonly property Item firstInvisibleItem: d.firstInvisibleIndex !== undefined ? rowRepeater.itemAt(d.firstInvisibleIndex) : null
309 
310  visible: d.firstInvisibleIndex != undefined
311  x: firstInvisibleItem ? firstInvisibleItem.x : 0
312 
313  height: parent.height
314  width: units.gu(4)
315 
316  onVisibleChanged: {
317  if (!visible && __popup) dismiss();
318  }
319 
320  Icon {
321  id: icon
322  width: units.gu(2)
323  height: units.gu(2)
324  anchors.centerIn: parent
325  color: theme.palette.normal.backgroundText
326  name: "toolkit_chevron-down_2gu"
327  }
328 
329  function show() {
330  if (!__popup) {
331  __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
332  __popup.childActivated.connect(dismiss);
333  // force the current item to be the newly popped up menu
334  } else {
335  __popup.show();
336  }
337  d.currentItem = overflowButton;
338  }
339  function hide() {
340  if (__popup) {
341  __popup.hide();
342 
343  if (d.currentItem === overflowButton) {
344  d.currentItem = null;
345  }
346  }
347  }
348  function dismiss() {
349  if (__popup) {
350  __popup.destroy();
351  __popup = null;
352 
353  if (d.currentItem === overflowButton) {
354  d.currentItem = null;
355  }
356  }
357  }
358 
359  Connections {
360  target: d
361  onDismissAll: overflowButton.dismiss()
362  }
363 
364  Component {
365  id: overflowComponent
366  MenuPopup {
367  id: overflowPopup
368  desiredX: overflowButton.x - units.gu(1)
369  desiredY: parent.height
370  unityMenuModel: overflowModel
371 
372  ExpressionFilterModel {
373  id: overflowModel
374  sourceModel: root.unityMenuModel
375  matchExpression: function(index) {
376  if (d.firstInvisibleIndex === undefined) return false;
377  return index >= d.firstInvisibleIndex;
378  }
379 
380  function submenu(index) {
381  return sourceModel.submenu(mapRowToSource(index));
382  }
383  function activate(index) {
384  return sourceModel.activate(mapRowToSource(index));
385  }
386  function aboutToShow(index) {
387  return sourceModel.aboutToShow(mapRowToSource(index));
388  }
389  }
390 
391  Connections {
392  target: d
393  onFirstInvisibleIndexChanged: overflowModel.invalidate()
394  }
395  }
396  }
397  }
398 
399  Rectangle {
400  id: underline
401  anchors {
402  bottom: row.bottom
403  }
404  x: d.currentItem ? row.x + d.currentItem.x : 0
405  width: d.currentItem ? d.currentItem.width : 0
406  height: units.dp(4)
407  color: UbuntuColors.orange
408  visible: d.currentItem
409  }
410 
411  MenuNavigator {
412  id: d
413  objectName: "d"
414  itemView: rowRepeater
415  hasOverflow: overflowButton.visible
416 
417  property Item currentItem: null
418  property Item hoveredItem: null
419  property Item prevCurrentItem: null
420  property bool altPressed: false
421  property bool longAltPressed: false
422  property var firstInvisibleIndex: undefined
423 
424  readonly property int currentIndex: currentItem && currentItem.hasOwnProperty("__ownIndex") ? currentItem.__ownIndex : -1
425 
426  signal dismissAll()
427 
428  function recalcFirstInvisibleIndexAdded(index, item) {
429  if (firstInvisibleIndex === undefined) {
430  if (!item.shouldDisplay) {
431  firstInvisibleIndex = index;
432  }
433  } else if (index <= firstInvisibleIndex) {
434  if (!item.shouldDisplay) {
435  firstInvisibleIndex = index;
436  } else {
437  firstInvisibleIndex++;
438  }
439  }
440  }
441 
442  function recalcFirstInvisibleIndex() {
443  for (var i = 0; i < rowRepeater.count; i++) {
444  if (!rowRepeater.itemAt(i).shouldDisplay) {
445  firstInvisibleIndex = i;
446  return;
447  }
448  }
449  firstInvisibleIndex = undefined;
450  }
451 
452  onSelect: {
453  var delegate = rowRepeater.itemAt(index);
454  if (delegate) {
455  d.currentItem = delegate;
456  }
457  }
458 
459  onOverflow: {
460  d.currentItem = overflowButton;
461  }
462 
463  onCurrentItemChanged: {
464  if (prevCurrentItem && prevCurrentItem != currentItem) {
465  if (currentItem) {
466  prevCurrentItem.hide();
467  } else {
468  prevCurrentItem.dismiss();
469  }
470  }
471 
472  if (currentItem) currentItem.show();
473  prevCurrentItem = currentItem;
474  }
475 
476  function startShortcutTimer() {
477  d.altPressed = true;
478  menuBarShortcutTimer.start();
479  }
480 
481  function stopSHortcutTimer() {
482  menuBarShortcutTimer.stop();
483  d.altPressed = false;
484  d.longAltPressed = false;
485  }
486  }
487 
488  Timer {
489  id: menuBarShortcutTimer
490  interval: 200
491  repeat: false
492  onTriggered: {
493  d.longAltPressed = true;
494  }
495  }
496 
497  Keys.onEscapePressed: {
498  d.dismissAll();
499  event.accepted = true;
500  }
501 
502  Keys.onLeftPressed: {
503  if (d.currentItem) {
504  d.selectPrevious(d.currentIndex);
505  }
506  }
507 
508  Keys.onRightPressed: {
509  if (d.currentItem) {
510  d.selectNext(d.currentIndex);
511  }
512  }
513 }