Lomiri
Loading...
Searching...
No Matches
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
17import QtQuick 2.12
18import QtQuick.Layouts 1.1
19import Utils 0.1
20import Lomiri.Components 1.3
21import GlobalShortcut 1.0
22import "../Components/PanelState"
23
24Item {
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}