Lomiri
Loading...
Searching...
No Matches
MenuPopup.qml
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 */
16
17import QtQuick 2.12
18import QtQuick.Layouts 1.1
19import Lomiri.Components 1.3
20import Lomiri.Components.ListItems 1.3 as ListItems
21import "../Components"
22import "../Components/PanelState"
23import "."
24
25LomiriShape {
26 id: root
27 objectName: "menu"
28 backgroundColor: theme.palette.normal.overlay
29
30 signal childActivated()
31
32 // true for submenus that need to show on the other side of their parent
33 // if they don't fit when growing right
34 property bool substractWidth: false
35
36 property bool selectFirstOnCountChange: true
37
38 property real desiredX
39 x: {
40 var dummy = visible; // force recalc when shown/hidden
41 var parentTopLeft = parent.mapToItem(null, 0, 0);
42 var farX = ApplicationMenusLimits.screenWidth;
43 if (parentTopLeft.x + width + desiredX <= farX) {
44 return desiredX;
45 } else {
46 if (substractWidth) {
47 return -width;
48 } else {
49 return farX - parentTopLeft.x - width;
50 }
51 }
52 }
53
54 property real desiredY
55 y: {
56 var dummy = visible; // force recalc when shown/hidden
57 var parentTopLeft = parent.mapToItem(null, 0, 0);
58 var bottomY = ApplicationMenusLimits.screenHeight;
59 if (parentTopLeft.y + height + desiredY <= bottomY) {
60 return desiredY;
61 } else {
62 return bottomY - parentTopLeft.y - height;
63 }
64 }
65
66 property alias lomiriMenuModel: repeater.model
67 property PanelState panelState
68
69 function show() {
70 visible = true;
71 focusScope.forceActiveFocus();
72 }
73
74 function hide() {
75 visible = false;
76 d.currentItem = null;
77 }
78
79 function selectFirstIndex() {
80 d.selectNext(-1);
81 }
82
83 function reset() {
84 d.currentItem = null;
85 dismiss();
86 }
87
88 function dismiss() {
89 d.dismissAll();
90 }
91
92 implicitWidth: focusScope.width
93 implicitHeight: focusScope.height
94
95 MenuNavigator {
96 id: d
97 objectName: "d"
98 itemView: repeater
99
100 property Item currentItem: null
101 property Item hoveredItem: null
102 readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
103
104 property real __minimumWidth: units.gu(20)
105 property real __maximumWidth: ApplicationMenusLimits.screenWidth * 0.7
106 property real __minimumHeight: units.gu(2)
107 property real __maximumHeight: panelState ? ApplicationMenusLimits.screenHeight - panelState.panelHeight : 0
108
109 signal dismissAll()
110
111 onCurrentItemChanged: {
112 if (currentItem) {
113 currentItem.item.forceActiveFocus();
114 } else {
115 hoveredItem = null;
116 }
117
118 submenuHoverTimer.stop();
119 }
120
121 onSelect: {
122 currentItem = repeater.itemAt(index);
123 if (currentItem) {
124 if (currentItem.y < listView.contentY) {
125 listView.contentY = currentItem.y;
126 } else if (currentItem.y + currentItem.height > listView.contentY + listView.height) {
127 listView.contentY = currentItem.y + currentItem.height - listView.height;
128 }
129 }
130 }
131 }
132
133 MouseArea {
134 // Eat events.
135 anchors.fill: parent
136 }
137
138 Item {
139 id: focusScope
140 width: container.width
141 height: container.height
142 focus: visible
143
144 Keys.onUpPressed: d.selectPrevious(d.currentIndex)
145 Keys.onDownPressed: d.selectNext(d.currentIndex)
146 Keys.onRightPressed: {
147 // Don't let right keypresses fall through if the current item has a visible popup.
148 if (!d.currentItem || !d.currentItem.popup || !d.currentItem.popup.visible) {
149 event.accepted = false;
150 }
151 }
152
153 ColumnLayout {
154 id: container
155 objectName: "container"
156
157 height: MathUtils.clamp(listView.contentHeight, d.__minimumHeight, d.__maximumHeight)
158 width: menuColumn.width
159 spacing: 0
160
161 // Header - scroll up
162 Item {
163 Layout.fillWidth: true
164 height: units.gu(3)
165 visible: listView.contentHeight > root.height
166 enabled: !listView.atYBeginning
167 z: 1
168
169 Rectangle {
170 color: enabled ? theme.palette.normal.overlayText :
171 theme.palette.disabled.overlayText
172 height: units.dp(1)
173 anchors {
174 bottom: parent.bottom
175 left: parent.left
176 right: parent.right
177 }
178 }
179
180 Icon {
181 anchors.centerIn: parent
182 width: units.gu(2)
183 height: units.gu(2)
184 name: "up"
185 color: enabled ? theme.palette.normal.overlayText :
186 theme.palette.disabled.overlayText
187 }
188
189 MouseArea {
190 id: previousMA
191 anchors.fill: parent
192 hoverEnabled: enabled
193 onPressed: progress()
194
195 Timer {
196 running: previousMA.containsMouse && !listView.atYBeginning
197 interval: 1000
198 repeat: true
199 onTriggered: previousMA.progress()
200 }
201
202 function progress() {
203 var item = menuColumn.childAt(0, listView.contentY);
204 if (item) {
205 var previousItem = item;
206 do {
207 previousItem = repeater.itemAt(previousItem.__ownIndex-1);
208 if (!previousItem) {
209 listView.contentY = 0;
210 return;
211 }
212 } while (previousItem.__isSeparator);
213
214 listView.contentY = previousItem.y
215 }
216 }
217 }
218 }
219
220 // Menu Items
221 Flickable {
222 id: listView
223 clip: interactive
224
225 Layout.fillHeight: true
226 Layout.fillWidth: true
227 contentHeight: menuColumn.height
228 interactive: height < contentHeight
229
230 Timer {
231 id: submenuHoverTimer
232 interval: 225 // GTK MENU_POPUP_DELAY, Qt SH_Menu_SubMenuPopupDelay in QCommonStyle is 256
233 onTriggered: d.currentItem.item.trigger();
234 }
235
236 MouseArea {
237 anchors.fill: parent
238 hoverEnabled: true
239 z: 1 // on top so we override any other hovers
240 onEntered: updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
241 onPositionChanged: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
242
243 function updateCurrentItemFromPosition(point) {
244 var pos = mapToItem(listView.contentItem, point.x, point.y);
245
246 if (!d.hoveredItem || !d.currentItem ||
247 !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
248 submenuHoverTimer.stop();
249
250 d.hoveredItem = menuColumn.childAt(pos.x, pos.y)
251 if (!d.hoveredItem || !d.hoveredItem.enabled)
252 return;
253 d.currentItem = d.hoveredItem;
254
255 if (!d.currentItem.__isSeparator && d.currentItem.item.hasSubmenu && d.currentItem.item.enabled) {
256 submenuHoverTimer.start();
257 }
258 }
259 }
260
261 onClicked: {
262 var pos = mapToItem(listView.contentItem, mouse.x, mouse.y);
263 var clickedItem = menuColumn.childAt(pos.x, pos.y);
264 if (clickedItem.enabled && !clickedItem.__isSeparator) {
265 clickedItem.item.trigger();
266 }
267 }
268 }
269
270 ActionContext {
271 id: menuBarContext
272 objectName: "menuContext"
273 active: {
274 if (!root.visible) return false;
275 if (d.currentItem && d.currentItem.popup && d.currentItem.popup.visible) {
276 return false;
277 }
278 return true;
279 }
280 }
281
282 Component {
283 id: separatorComponent
284 ListItems.ThinDivider {
285 // Parent will be loader
286 objectName: parent.objectName + "-separator"
287 implicitHeight: units.dp(2)
288 }
289 }
290
291 Component {
292 id: menuItemComponent
293 MenuItem {
294 // Parent will be loader
295 id: menuItem
296 menuData: parent.__menuData
297 objectName: parent.objectName + "-actionItem"
298
299 width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
300
301 property Item popup: null
302
303 action.onTriggered: {
304 submenuHoverTimer.stop();
305
306 d.currentItem = parent;
307
308 if (hasSubmenu) {
309 if (!popup) {
310 root.lomiriMenuModel.aboutToShow(__ownIndex);
311 var model = root.lomiriMenuModel.submenu(__ownIndex);
312 popup = submenuComponent.createObject(focusScope, {
313 objectName: parent.objectName + "-",
314 lomiriMenuModel: model,
315 substractWidth: true,
316 desiredX: Qt.binding(function() { return root.width }),
317 desiredY: Qt.binding(function() {
318 var dummy = listView.contentY; // force a recalc on contentY change.
319 return mapToItem(container, 0, y).y;
320 })
321 });
322 popup.retreat.connect(function() {
323 popup.destroy();
324 popup = null;
325 menuItem.forceActiveFocus();
326 });
327 popup.childActivated.connect(function() {
328 popup.destroy();
329 popup = null;
330 root.childActivated();
331 });
332 } else if (!popup.visible) {
333 root.lomiriMenuModel.aboutToShow(__ownIndex);
334 popup.visible = true;
335 popup.item.selectFirstIndex();
336 }
337 } else {
338 root.lomiriMenuModel.activate(__ownIndex);
339 root.childActivated();
340 }
341 }
342
343 Connections {
344 target: d
345 onCurrentIndexChanged: {
346 if (popup && d.currentIndex != __ownIndex) {
347 popup.visible = false;
348 }
349 }
350 onDismissAll: {
351 if (popup) {
352 popup.destroy();
353 popup = null;
354 }
355 }
356 }
357
358 Component.onDestruction: {
359 if (popup) {
360 popup.destroy();
361 popup = null;
362 }
363 }
364 }
365 }
366
367 ColumnLayout {
368 id: menuColumn
369 spacing: 0
370
371 width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
372
373 Repeater {
374 id: repeater
375
376 onCountChanged: {
377 if (root.selectFirstOnCountChange && !d.currentItem && count > 0) {
378 root.selectFirstIndex();
379 }
380 }
381
382 Loader {
383 id: loader
384 objectName: root.objectName + "-item" + __ownIndex
385
386 readonly property var popup: item ? item.popup : null
387 property var __menuData: model
388 property int __ownIndex: index
389 property bool __isSeparator: model.isSeparator
390
391 enabled: __isSeparator ? false : model.sensitive
392
393 sourceComponent: {
394 if (model.isSeparator) {
395 return separatorComponent;
396 }
397 return menuItemComponent;
398 }
399
400 Layout.fillWidth: true
401 }
402
403 }
404 }
405
406 // Highlight
407 Rectangle {
408 color: "transparent"
409 border.width: units.dp(1)
410 border.color: LomiriColors.orange
411 z: 1
412
413 width: listView.width
414 height: d.currentItem ? d.currentItem.height : 0
415 y: d.currentItem ? d.currentItem.y : 0
416 visible: d.currentItem
417 }
418
419 } // Flickable
420
421 // Header - scroll down
422 Item {
423 Layout.fillWidth: true
424 height: units.gu(3)
425 visible: listView.contentHeight > root.height
426 enabled: !listView.atYEnd
427 z: 1
428
429 Rectangle {
430 color: enabled ? theme.palette.normal.overlayText :
431 theme.palette.disabled.overlayText
432 height: units.dp(1)
433 anchors {
434 top: parent.top
435 left: parent.left
436 right: parent.right
437 }
438 }
439
440 Icon {
441 anchors.centerIn: parent
442 width: units.gu(2)
443 height: units.gu(2)
444 name: "down"
445 color: enabled ? theme.palette.normal.overlayText :
446 theme.palette.disabled.overlayText
447 }
448
449 MouseArea {
450 id: nextMA
451 anchors.fill: parent
452 hoverEnabled: enabled
453 onPressed: progress()
454
455 Timer {
456 running: nextMA.containsMouse && !listView.atYEnd
457 interval: 1000
458 repeat: true
459 onTriggered: nextMA.progress()
460 }
461
462 function progress() {
463 var item = menuColumn.childAt(0, listView.contentY + listView.height);
464 if (item) {
465 var nextItem = item;
466 do {
467 nextItem = repeater.itemAt(nextItem.__ownIndex+1);
468 if (!nextItem) {
469 listView.contentY = listView.contentHeight - listView.height;
470 return;
471 }
472 } while (nextItem.__isSeparator);
473
474 listView.contentY = nextItem.y - listView.height
475 }
476 }
477 }
478 }
479 } // Column
480
481 Component {
482 id: submenuComponent
483 Loader {
484 id: submenuLoader
485 source: "MenuPopup.qml"
486
487 property real desiredX
488 property real desiredY
489 property bool substractWidth
490 property var lomiriMenuModel: null
491 signal retreat()
492 signal childActivated()
493
494 onLoaded: {
495 item.lomiriMenuModel = Qt.binding(function() { return submenuLoader.lomiriMenuModel; });
496 item.panelState = Qt.binding(function() { return root.panelState; });
497 item.objectName = Qt.binding(function() { return submenuLoader.objectName + "menu"; });
498 item.desiredX = Qt.binding(function() { return submenuLoader.desiredX; });
499 item.desiredY = Qt.binding(function() { return submenuLoader.desiredY; });
500 item.substractWidth = Qt.binding(function() { return submenuLoader.substractWidth; });
501 }
502
503 Keys.onLeftPressed: retreat()
504
505 Connections {
506 target: item
507 onChildActivated: childActivated();
508 }
509 }
510 }
511 }
512}