on bb09f86 help sign in
468 lines 15 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
32 // read from outside
33 readonly property bool valid: rowRepeater.count > 0
34 readonly property bool showRequested: d.longAltPressed || d.currentItem != null
36 implicitWidth: row.width
37 height: parent.height
39 function select(index) {
40 d.select(index);
41 }
43 function dismiss() {
44 d.dismissAll();
45 }
47 GlobalShortcut {
48 shortcut: Qt.Key_Alt|Qt.AltModifier
49 active: enableKeyFilter
50 onTriggered: d.startShortcutTimer()
51 onReleased: d.stopSHortcutTimer()
52 }
53 // On an actual keyboard, the AltModifier is not supplied on release.
54 GlobalShortcut {
55 shortcut: Qt.Key_Alt
56 active: enableKeyFilter
57 onTriggered: d.startShortcutTimer()
58 onReleased: d.stopSHortcutTimer()
59 }
61 GlobalShortcut {
62 shortcut: Qt.AltModifier | Qt.Key_F10
63 active: enableKeyFilter && d.currentItem == null
64 onTriggered: {
65 for (var i = 0; i < rowRepeater.count; i++) {
66 var item = rowRepeater.itemAt(i);
67 if (item.enabled) {
68 item.show();
69 break;
70 }
71 }
72 }
73 }
75 InverseMouseArea {
76 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
77 anchors.fill: parent
78 enabled: d.currentItem != null
79 hoverEnabled: enabled && d.currentItem && d.currentItem.__popup != null
80 onPressed: d.dismissAll()
81 }
83 Row {
84 id: row
85 spacing: units.gu(2)
86 height: parent.height
88 ActionContext {
89 id: menuBarContext
90 objectName: "barContext"
91 active: !d.currentItem && enableKeyFilter
92 }
94 Connections {
95 target: root.unityMenuModel
96 onModelReset: d.firstInvisibleIndex = undefined
97 }
99 Repeater {
100 id: rowRepeater
102 onItemAdded: d.recalcFirstInvisibleIndexAdded(index, item)
103 onCountChanged: d.recalcFirstInvisibleIndex()
105 Item {
106 id: visualItem
107 objectName: root.objectName + "-item" + __ownIndex
109 readonly property int __ownIndex: index
110 property Item __popup: null;
111 readonly property bool popupVisible: __popup && __popup.visible
112 readonly property bool shouldDisplay: x + width + ((__ownIndex < rowRepeater.count-1) ? units.gu(2) : 0) <
113 root.overflowWidth - ((__ownIndex < rowRepeater.count-1) ? overflowButton.width : 0)
115 implicitWidth: column.implicitWidth
116 implicitHeight: row.height
117 enabled: model.sensitive && shouldDisplay
118 opacity: shouldDisplay ? 1 : 0
120 function show() {
121 if (!__popup) {
122 __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
123 __popup.childActivated.connect(dismiss);
124 // force the current item to be the newly popped up menu
125 } else {
126 __popup.show();
127 }
128 d.currentItem = visualItem;
129 }
130 function hide() {
131 if (__popup) {
132 __popup.hide();
134 if (d.currentItem === visualItem) {
135 d.currentItem = null;
136 }
137 }
138 }
139 function dismiss() {
140 if (__popup) {
141 __popup.destroy();
142 __popup = null;
144 if (d.currentItem === visualItem) {
145 d.currentItem = null;
146 }
147 }
148 }
150 onVisibleChanged: {
151 if (!visible && __popup) dismiss();
152 }
154 onShouldDisplayChanged: {
155 if ((!shouldDisplay && d.firstInvisibleIndex == undefined) || __ownIndex <= d.firstInvisibleIndex) {
156 d.recalcFirstInvisibleIndex();
157 }
158 }
160 Connections {
161 target: d
162 onDismissAll: visualItem.dismiss()
163 }
165 Component {
166 id: menuComponent
167 MenuPopup {
168 desiredX: visualItem.x - units.gu(1)
169 desiredY: parent.height
170 unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
172 Component.onCompleted: reset();
173 }
174 }
176 RowLayout {
177 id: column
178 spacing: units.gu(1)
179 anchors {
180 centerIn: parent
181 }
183 Icon {
184 Layout.preferredWidth: units.gu(2)
185 Layout.preferredHeight: units.gu(2)
186 Layout.alignment: Qt.AlignVCenter
188 visible: model.icon || false
189 source: model.icon || ""
190 }
192 ActionItem {
193 id: actionItem
194 width: _title.width
195 height: _title.height
197 action: Action {
198 enabled: visualItem.enabled
199 // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
200 text: model.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
202 onTriggered: {
203 visualItem.show();
204 }
205 }
207 Label {
208 id: _title
209 text: actionItem.text
210 horizontalAlignment: Text.AlignLeft
211 color: enabled ? theme.palette.normal.backgroundText : theme.palette.disabled.backgroundText
212 }
213 }
214 }
215 } // Item ( delegate )
216 } // Repeater
217 } // Row
219 MouseArea {
220 anchors.fill: row
221 hoverEnabled: d.currentItem
223 onEntered: {
224 if (d.currentItem) {
225 updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
226 }
227 }
228 onPositionChanged: {
229 if (d.currentItem) {
230 updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
231 }
232 }
233 onClicked: {
234 var prevItem = d.currentItem;
235 updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
236 if (prevItem && d.currentItem == prevItem) {
237 prevItem.hide();
238 }
239 }
241 function updateCurrentItemFromPosition(point) {
242 var pos = mapToItem(row, point.x, point.y);
244 if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
245 d.hoveredItem = row.childAt(pos.x, pos.y);
246 if (!d.hoveredItem || !d.hoveredItem.enabled)
247 return;
248 if (d.currentItem != d.hoveredItem) {
249 d.currentItem = d.hoveredItem;
250 }
251 }
252 }
253 }
255 MouseArea {
256 id: overflowButton
257 objectName: "overflow"
259 hoverEnabled: d.currentItem
260 onEntered: d.currentItem = this
261 onPositionChanged: d.currentItem = this
262 onClicked: d.currentItem = this
264 property Item __popup: null;
265 readonly property bool popupVisible: __popup && __popup.visible
266 readonly property Item firstInvisibleItem: d.firstInvisibleIndex !== undefined ? rowRepeater.itemAt(d.firstInvisibleIndex) : null
268 visible: d.firstInvisibleIndex != undefined
269 x: firstInvisibleItem ? firstInvisibleItem.x : 0
271 height: parent.height
272 width: units.gu(4)
274 onVisibleChanged: {
275 if (!visible && __popup) dismiss();
276 }
278 Icon {
279 id: icon
280 width: units.gu(2)
281 height: units.gu(2)
282 anchors.centerIn: parent
283 color: theme.palette.normal.backgroundText
284 name: "toolkit_chevron-down_2gu"
285 }
287 function show() {
288 if (!__popup) {
289 __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
290 __popup.childActivated.connect(dismiss);
291 // force the current item to be the newly popped up menu
292 } else {
293 __popup.show();
294 }
295 d.currentItem = overflowButton;
296 }
297 function hide() {
298 if (__popup) {
299 __popup.hide();
301 if (d.currentItem === overflowButton) {
302 d.currentItem = null;
303 }
304 }
305 }
306 function dismiss() {
307 if (__popup) {
308 __popup.destroy();
309 __popup = null;
311 if (d.currentItem === overflowButton) {
312 d.currentItem = null;
313 }
314 }
315 }
317 Connections {
318 target: d
319 onDismissAll: overflowButton.dismiss()
320 }
322 Component {
323 id: overflowComponent
324 MenuPopup {
325 id: overflowPopup
326 desiredX: overflowButton.x - units.gu(1)
327 desiredY: parent.height
328 unityMenuModel: overflowModel
330 ExpressionFilterModel {
331 id: overflowModel
332 sourceModel: root.unityMenuModel
333 matchExpression: function(index) {
334 if (d.firstInvisibleIndex === undefined) return false;
335 return index >= d.firstInvisibleIndex;
336 }
338 function submenu(index) {
339 return sourceModel.submenu(mapRowToSource(index));
340 }
341 function activate(index) {
342 return sourceModel.activate(mapRowToSource(index));
343 }
344 }
346 Connections {
347 target: d
348 onFirstInvisibleIndexChanged: overflowModel.invalidate()
349 }
350 }
351 }
352 }
354 Rectangle {
355 id: underline
356 anchors {
357 bottom: row.bottom
358 }
359 x: d.currentItem ? row.x + d.currentItem.x - units.gu(1) : 0
360 width: d.currentItem ? d.currentItem.width + units.gu(2) : 0
361 height: units.dp(4)
362 color: UbuntuColors.orange
363 visible: d.currentItem
364 }
366 MenuNavigator {
367 id: d
368 objectName: "d"
369 itemView: rowRepeater
370 hasOverflow: overflowButton.visible
372 property Item currentItem: null
373 property Item hoveredItem: null
374 property Item prevCurrentItem: null
375 property bool altPressed: false
376 property bool longAltPressed: false
377 property var firstInvisibleIndex: undefined
379 readonly property int currentIndex: currentItem && currentItem.hasOwnProperty("__ownIndex") ? currentItem.__ownIndex : -1
381 signal dismissAll()
383 function recalcFirstInvisibleIndexAdded(index, item) {
384 if (firstInvisibleIndex === undefined) {
385 if (!item.shouldDisplay) {
386 firstInvisibleIndex = index;
387 }
388 } else if (index <= firstInvisibleIndex) {
389 if (!item.shouldDisplay) {
390 firstInvisibleIndex = index;
391 } else {
392 firstInvisibleIndex++;
393 }
394 }
395 }
397 function recalcFirstInvisibleIndex() {
398 for (var i = 0; i < rowRepeater.count; i++) {
399 if (!rowRepeater.itemAt(i).shouldDisplay) {
400 firstInvisibleIndex = i;
401 return;
402 }
403 }
404 firstInvisibleIndex = undefined;
405 }
407 onSelect: {
408 var delegate = rowRepeater.itemAt(index);
409 if (delegate) {
410 d.currentItem = delegate;
411 }
412 }
414 onOverflow: {
415 d.currentItem = overflowButton;
416 }
418 onCurrentItemChanged: {
419 if (prevCurrentItem && prevCurrentItem != currentItem) {
420 if (currentItem) {
421 prevCurrentItem.hide();
422 } else {
423 prevCurrentItem.dismiss();
424 }
425 }
427 if (currentItem) currentItem.show();
428 prevCurrentItem = currentItem;
429 }
431 function startShortcutTimer() {
432 d.altPressed = true;
433 menuBarShortcutTimer.start();
434 }
436 function stopSHortcutTimer() {
437 menuBarShortcutTimer.stop();
438 d.altPressed = false;
439 d.longAltPressed = false;
440 }
441 }
443 Timer {
444 id: menuBarShortcutTimer
445 interval: 200
446 repeat: false
447 onTriggered: {
448 d.longAltPressed = true;
449 }
450 }
452 Keys.onEscapePressed: {
453 d.dismissAll();
454 event.accepted = true;
455 }
457 Keys.onLeftPressed: {
458 if (d.currentItem) {
459 d.selectPrevious(d.currentIndex);
460 }
461 }
463 Keys.onRightPressed: {
464 if (d.currentItem) {
465 d.selectNext(d.currentIndex);
466 }
467 }