on 5ea3c69 help sign in
513 lines 17 kB history raw
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 */
17import QtQuick 2.4
18import QtQuick.Layouts 1.1
19import Utils 0.1
20import Ubuntu.Components 1.3
21import GlobalShortcut 1.0
23Item {
24 id: root
25 objectName: "menuBar"
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
33 // read from outside
34 readonly property bool valid: rowRepeater.count > 0
35 readonly property bool showRequested: d.longAltPressed || d.currentItem != null
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)
44 implicitWidth: row.width
45 height: parent.height
47 function dismiss() {
48 d.dismissAll();
49 }
51 function invokeMenu(mouseEvent) {
52 mouseArea.onClicked(mouseEvent);
53 }
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 }
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 }
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 }
91 Row {
92 id: row
93 spacing: 0
94 height: parent.height
96 ActionContext {
97 id: menuBarContext
98 objectName: "barContext"
99 active: !d.currentItem && enableKeyFilter
100 }
102 Connections {
103 target: root.unityMenuModel
104 onModelReset: d.firstInvisibleIndex = undefined
105 }
107 Component {
108 id: menuComponent
109 MenuPopup { }
110 }
112 Repeater {
113 id: rowRepeater
115 onItemAdded: d.recalcFirstInvisibleIndexAdded(index, item)
116 onCountChanged: d.recalcFirstInvisibleIndex()
118 Item {
119 id: visualItem
120 objectName: root.objectName + "-item" + __ownIndex
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)
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
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
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();
161 if (d.currentItem === visualItem) {
162 d.currentItem = null;
163 }
164 }
165 }
166 function dismiss() {
167 if (__popup) {
168 __popup.destroy();
169 __popup = null;
171 if (d.currentItem === visualItem) {
172 d.currentItem = null;
173 }
174 }
175 }
177 onVisibleChanged: {
178 if (!visible && __popup) dismiss();
179 }
181 onShouldDisplayChanged: {
182 if ((!shouldDisplay && d.firstInvisibleIndex == undefined) || __ownIndex <= d.firstInvisibleIndex) {
183 d.recalcFirstInvisibleIndex();
184 }
185 }
187 Connections {
188 target: d
189 onDismissAll: visualItem.dismiss()
190 }
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 }
201 Icon {
202 Layout.preferredWidth: units.gu(2)
203 Layout.preferredHeight: units.gu(2)
204 Layout.alignment: Qt.AlignVCenter
206 visible: model.icon || false
207 source: model.icon || ""
208 }
210 ActionItem {
211 id: actionItem
212 width: _title.width
213 height: _title.height
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>", "")
220 onTriggered: {
221 visualItem.show();
222 }
223 }
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 }
234 Component.onDestruction: {
235 if (__popup) {
236 __popup.destroy();
237 __popup = null;
238 }
239 }
240 } // Item ( delegate )
241 } // Repeater
242 } // Row
244 MouseArea {
245 id: mouseArea
246 anchors.fill: parent
247 hoverEnabled: d.currentItem
249 property bool moved: false
251 onEntered: {
252 if (d.currentItem) {
253 updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
254 }
255 }
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 }
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)
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 }
283 function updateCurrentItemFromPosition(point) {
284 var pos = mapToItem(row, point.x, point.y);
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 }
297 MouseArea {
298 id: overflowButton
299 objectName: "overflow"
301 hoverEnabled: d.currentItem
302 onEntered: d.currentItem = this
303 onPositionChanged: d.currentItem = this
304 onPressed: d.currentItem = this
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
310 visible: d.firstInvisibleIndex != undefined
311 x: firstInvisibleItem ? firstInvisibleItem.x : 0
313 height: parent.height
314 width: units.gu(4)
316 onVisibleChanged: {
317 if (!visible && __popup) dismiss();
318 }
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 }
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();
343 if (d.currentItem === overflowButton) {
344 d.currentItem = null;
345 }
346 }
347 }
348 function dismiss() {
349 if (__popup) {
350 __popup.destroy();
351 __popup = null;
353 if (d.currentItem === overflowButton) {
354 d.currentItem = null;
355 }
356 }
357 }
359 Connections {
360 target: d
361 onDismissAll: overflowButton.dismiss()
362 }
364 Component {
365 id: overflowComponent
366 MenuPopup {
367 id: overflowPopup
368 desiredX: overflowButton.x - units.gu(1)
369 desiredY: parent.height
370 unityMenuModel: overflowModel
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 }
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 }
391 Connections {
392 target: d
393 onFirstInvisibleIndexChanged: overflowModel.invalidate()
394 }
395 }
396 }
397 }
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 }
411 MenuNavigator {
412 id: d
413 objectName: "d"
414 itemView: rowRepeater
415 hasOverflow: overflowButton.visible
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
424 readonly property int currentIndex: currentItem && currentItem.hasOwnProperty("__ownIndex") ? currentItem.__ownIndex : -1
426 signal dismissAll()
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 }
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 }
452 onSelect: {
453 var delegate = rowRepeater.itemAt(index);
454 if (delegate) {
455 d.currentItem = delegate;
456 }
457 }
459 onOverflow: {
460 d.currentItem = overflowButton;
461 }
463 onCurrentItemChanged: {
464 if (prevCurrentItem && prevCurrentItem != currentItem) {
465 if (currentItem) {
466 prevCurrentItem.hide();
467 } else {
468 prevCurrentItem.dismiss();
469 }
470 }
472 if (currentItem) currentItem.show();
473 prevCurrentItem = currentItem;
474 }
476 function startShortcutTimer() {
477 d.altPressed = true;
478 menuBarShortcutTimer.start();
479 }
481 function stopSHortcutTimer() {
482 menuBarShortcutTimer.stop();
483 d.altPressed = false;
484 d.longAltPressed = false;
485 }
486 }
488 Timer {
489 id: menuBarShortcutTimer
490 interval: 200
491 repeat: false
492 onTriggered: {
493 d.longAltPressed = true;
494 }
495 }
497 Keys.onEscapePressed: {
498 d.dismissAll();
499 event.accepted = true;
500 }
502 Keys.onLeftPressed: {
503 if (d.currentItem) {
504 d.selectPrevious(d.currentIndex);
505 }
506 }
508 Keys.onRightPressed: {
509 if (d.currentItem) {
510 d.selectNext(d.currentIndex);
511 }
512 }