on fe9d37e help sign in
493 lines 16 kB history raw
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 */
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 select(index) {
48 d.select(index);
49 }
51 function dismiss() {
52 d.dismissAll();
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: units.gu(2)
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 implicitWidth: column.implicitWidth
129 implicitHeight: row.height
130 enabled: (model.sensitive === true) && shouldDisplay
131 opacity: shouldDisplay ? 1 : 0
133 function show() {
134 if (!__popup) {
135 __popup = menuComponent.createObject(root,
136 {
137 objectName: visualItem.objectName + "-menu",
138 desiredX: Qt.binding(function() { return visualItem.x - units.gu(1); }),
139 desiredY: Qt.binding(function() { return root.height; }),
140 unityMenuModel: Qt.binding(function() { return root.unityMenuModel.submenu(visualItem.__ownIndex); })
141 });
142 __popup.reset();
143 __popup.childActivated.connect(dismiss);
144 // force the current item to be the newly popped up menu
145 } else {
146 __popup.show();
147 }
148 d.currentItem = visualItem;
149 }
150 function hide() {
151 if (__popup) {
152 __popup.hide();
154 if (d.currentItem === visualItem) {
155 d.currentItem = null;
156 }
157 }
158 }
159 function dismiss() {
160 if (__popup) {
161 __popup.destroy();
162 __popup = null;
164 if (d.currentItem === visualItem) {
165 d.currentItem = null;
166 }
167 }
168 }
170 onVisibleChanged: {
171 if (!visible && __popup) dismiss();
172 }
174 onShouldDisplayChanged: {
175 if ((!shouldDisplay && d.firstInvisibleIndex == undefined) || __ownIndex <= d.firstInvisibleIndex) {
176 d.recalcFirstInvisibleIndex();
177 }
178 }
180 Connections {
181 target: d
182 onDismissAll: visualItem.dismiss()
183 }
185 RowLayout {
186 id: column
187 spacing: units.gu(1)
188 anchors {
189 centerIn: parent
190 }
192 Icon {
193 Layout.preferredWidth: units.gu(2)
194 Layout.preferredHeight: units.gu(2)
195 Layout.alignment: Qt.AlignVCenter
197 visible: model.icon || false
198 source: model.icon || ""
199 }
201 ActionItem {
202 id: actionItem
203 width: _title.width
204 height: _title.height
206 action: Action {
207 enabled: visualItem.enabled
208 // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
209 text: model.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
211 onTriggered: {
212 visualItem.show();
213 }
214 }
216 Label {
217 id: _title
218 text: actionItem.text
219 horizontalAlignment: Text.AlignLeft
220 color: enabled ? theme.palette.normal.backgroundText : theme.palette.disabled.backgroundText
221 }
222 }
223 }
224 } // Item ( delegate )
225 } // Repeater
226 } // Row
228 MouseArea {
229 anchors.fill: parent
230 hoverEnabled: d.currentItem
232 property bool moved: false
234 onEntered: {
235 if (d.currentItem) {
236 updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
237 }
238 }
240 onClicked: {
241 if (!moved) {
242 var prevItem = d.currentItem;
243 updateCurrentItemFromPosition(Qt.point(mouseX, mouseY));
244 if (prevItem && d.currentItem == prevItem) {
245 prevItem.hide();
246 }
247 }
248 moved = false;
249 }
251 // for the MoveHandler
252 onPressed: root.pressed(mouse)
253 onPressedChanged: root.pressedChangedEx(pressed, pressedButtons, mouseX, mouseY)
254 onReleased: root.released(mouse)
255 onDoubleClicked: root.doubleClicked(mouse)
257 Mouse.ignoreSynthesizedEvents: true
258 Mouse.onPositionChanged: {
259 root.positionChanged(mouse);
260 moved = root.windowMoving;
261 if (d.currentItem) {
262 updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
263 }
264 }
266 function updateCurrentItemFromPosition(point) {
267 var pos = mapToItem(row, point.x, point.y);
269 if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
270 d.hoveredItem = row.childAt(pos.x, pos.y);
271 if (!d.hoveredItem || !d.hoveredItem.enabled)
272 return;
273 if (d.currentItem != d.hoveredItem) {
274 d.currentItem = d.hoveredItem;
275 }
276 }
277 }
278 }
280 MouseArea {
281 id: overflowButton
282 objectName: "overflow"
284 hoverEnabled: d.currentItem
285 onEntered: d.currentItem = this
286 onPositionChanged: d.currentItem = this
287 onPressed: d.currentItem = this
289 property Item __popup: null;
290 readonly property bool popupVisible: __popup && __popup.visible
291 readonly property Item firstInvisibleItem: d.firstInvisibleIndex !== undefined ? rowRepeater.itemAt(d.firstInvisibleIndex) : null
293 visible: d.firstInvisibleIndex != undefined
294 x: firstInvisibleItem ? firstInvisibleItem.x : 0
296 height: parent.height
297 width: units.gu(4)
299 onVisibleChanged: {
300 if (!visible && __popup) dismiss();
301 }
303 Icon {
304 id: icon
305 width: units.gu(2)
306 height: units.gu(2)
307 anchors.centerIn: parent
308 color: theme.palette.normal.backgroundText
309 name: "toolkit_chevron-down_2gu"
310 }
312 function show() {
313 if (!__popup) {
314 __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
315 __popup.childActivated.connect(dismiss);
316 // force the current item to be the newly popped up menu
317 } else {
318 __popup.show();
319 }
320 d.currentItem = overflowButton;
321 }
322 function hide() {
323 if (__popup) {
324 __popup.hide();
326 if (d.currentItem === overflowButton) {
327 d.currentItem = null;
328 }
329 }
330 }
331 function dismiss() {
332 if (__popup) {
333 __popup.destroy();
334 __popup = null;
336 if (d.currentItem === overflowButton) {
337 d.currentItem = null;
338 }
339 }
340 }
342 Connections {
343 target: d
344 onDismissAll: overflowButton.dismiss()
345 }
347 Component {
348 id: overflowComponent
349 MenuPopup {
350 id: overflowPopup
351 desiredX: overflowButton.x - units.gu(1)
352 desiredY: parent.height
353 unityMenuModel: overflowModel
355 ExpressionFilterModel {
356 id: overflowModel
357 sourceModel: root.unityMenuModel
358 matchExpression: function(index) {
359 if (d.firstInvisibleIndex === undefined) return false;
360 return index >= d.firstInvisibleIndex;
361 }
363 function submenu(index) {
364 return sourceModel.submenu(mapRowToSource(index));
365 }
366 function activate(index) {
367 return sourceModel.activate(mapRowToSource(index));
368 }
369 }
371 Connections {
372 target: d
373 onFirstInvisibleIndexChanged: overflowModel.invalidate()
374 }
375 }
376 }
377 }
379 Rectangle {
380 id: underline
381 anchors {
382 bottom: row.bottom
383 }
384 x: d.currentItem ? row.x + d.currentItem.x - units.gu(1) : 0
385 width: d.currentItem ? d.currentItem.width + units.gu(2) : 0
386 height: units.dp(4)
387 color: UbuntuColors.orange
388 visible: d.currentItem
389 }
391 MenuNavigator {
392 id: d
393 objectName: "d"
394 itemView: rowRepeater
395 hasOverflow: overflowButton.visible
397 property Item currentItem: null
398 property Item hoveredItem: null
399 property Item prevCurrentItem: null
400 property bool altPressed: false
401 property bool longAltPressed: false
402 property var firstInvisibleIndex: undefined
404 readonly property int currentIndex: currentItem && currentItem.hasOwnProperty("__ownIndex") ? currentItem.__ownIndex : -1
406 signal dismissAll()
408 function recalcFirstInvisibleIndexAdded(index, item) {
409 if (firstInvisibleIndex === undefined) {
410 if (!item.shouldDisplay) {
411 firstInvisibleIndex = index;
412 }
413 } else if (index <= firstInvisibleIndex) {
414 if (!item.shouldDisplay) {
415 firstInvisibleIndex = index;
416 } else {
417 firstInvisibleIndex++;
418 }
419 }
420 }
422 function recalcFirstInvisibleIndex() {
423 for (var i = 0; i < rowRepeater.count; i++) {
424 if (!rowRepeater.itemAt(i).shouldDisplay) {
425 firstInvisibleIndex = i;
426 return;
427 }
428 }
429 firstInvisibleIndex = undefined;
430 }
432 onSelect: {
433 var delegate = rowRepeater.itemAt(index);
434 if (delegate) {
435 d.currentItem = delegate;
436 }
437 }
439 onOverflow: {
440 d.currentItem = overflowButton;
441 }
443 onCurrentItemChanged: {
444 if (prevCurrentItem && prevCurrentItem != currentItem) {
445 if (currentItem) {
446 prevCurrentItem.hide();
447 } else {
448 prevCurrentItem.dismiss();
449 }
450 }
452 if (currentItem) currentItem.show();
453 prevCurrentItem = currentItem;
454 }
456 function startShortcutTimer() {
457 d.altPressed = true;
458 menuBarShortcutTimer.start();
459 }
461 function stopSHortcutTimer() {
462 menuBarShortcutTimer.stop();
463 d.altPressed = false;
464 d.longAltPressed = false;
465 }
466 }
468 Timer {
469 id: menuBarShortcutTimer
470 interval: 200
471 repeat: false
472 onTriggered: {
473 d.longAltPressed = true;
474 }
475 }
477 Keys.onEscapePressed: {
478 d.dismissAll();
479 event.accepted = true;
480 }
482 Keys.onLeftPressed: {
483 if (d.currentItem) {
484 d.selectPrevious(d.currentIndex);
485 }
486 }
488 Keys.onRightPressed: {
489 if (d.currentItem) {
490 d.selectNext(d.currentIndex);
491 }
492 }