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