Lomiri
Panel.qml
1 /*
2  * Copyright (C) 2013-2017 Canonical Ltd.
3  * Copyright (C) 2020 UBports Foundation
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 import QtQuick 2.4
19 import Lomiri.Components 1.3
20 import Lomiri.Layouts 1.0
21 import QtMir.Application 0.1
22 import Lomiri.Indicators 0.1
23 import Utils 0.1
24 import Lomiri.ApplicationMenu 0.1
25 
26 import QtQuick.Window 2.2
27 
28 import "../ApplicationMenus"
29 import "../Components"
30 import "../Components/PanelState"
31 import ".."
32 import "Indicators"
33 
34 Item {
35  id: root
36  readonly property real panelHeight: panelArea.y + minimizedPanelHeight
37  readonly property bool fullyClosed: indicators.fullyClosed && applicationMenus.fullyClosed
38 
39  property real minimizedPanelHeight: units.gu(3)
40  property real expandedPanelHeight: units.gu(7)
41  property real menuWidth: partialWidth ? units.gu(40) : width
42  property alias applicationMenuContentX: __applicationMenus.menuContentX
43 
44  property alias applicationMenus: __applicationMenus
45  property alias indicators: __indicators
46  property bool fullscreenMode: false
47  property real panelAreaShowProgress: 1.0
48  property bool greeterShown: false
49  property bool hasKeyboard: false
50  property bool supportsMultiColorLed: true
51 
52  // Whether our expanded menus should take up the full width of the panel
53  property bool partialWidth: width >= units.gu(60)
54 
55  property string mode: "staged"
56  property PanelState panelState
57 
58  MouseArea {
59  id: backMouseEater
60  anchors.fill: parent
61  anchors.topMargin: panelHeight
62  visible: !indicators.fullyClosed || !applicationMenus.fullyClosed
63  enabled: visible
64  hoverEnabled: true // should also eat hover events, otherwise they will pass through
65 
66  onClicked: {
67  __applicationMenus.hide();
68  __indicators.hide();
69  }
70  }
71 
72  Binding {
73  target: panelState
74  property: "panelHeight"
75  value: minimizedPanelHeight
76  }
77 
78  RegisteredApplicationMenuModel {
79  id: registeredMenuModel
80  persistentSurfaceId: panelState.focusedPersistentSurfaceId
81  }
82 
83  QtObject {
84  id: d
85 
86  property bool revealControls: !greeterShown &&
87  !applicationMenus.shown &&
88  !indicators.shown &&
89  (decorationMouseArea.containsMouse || menuBarLoader.menusRequested)
90 
91  property bool showWindowDecorationControls: (revealControls && panelState.decorationsVisible) ||
92  panelState.decorationsAlwaysVisible
93 
94  property bool showPointerMenu: revealControls &&
95  (panelState.decorationsVisible || mode == "windowed")
96 
97  property bool enablePointerMenu: applicationMenus.available &&
98  applicationMenus.model
99 
100  property bool showTouchMenu: !greeterShown &&
101  !showPointerMenu &&
102  !showWindowDecorationControls
103 
104  property bool enableTouchMenus: showTouchMenu &&
105  applicationMenus.available &&
106  applicationMenus.model
107  }
108 
109  Item {
110  id: panelArea
111  objectName: "panelArea"
112 
113  anchors.fill: parent
114 
115  transform: Translate {
116  y: indicators.state === "initial"
117  ? (1.0 - panelAreaShowProgress) * - minimizedPanelHeight
118  : 0
119  }
120 
121  BorderImage {
122  id: indicatorsDropShadow
123  anchors {
124  fill: __indicators
125  margins: -units.gu(1)
126  }
127  visible: !__indicators.fullyClosed
128  source: "graphics/rectangular_dropshadow.sci"
129  }
130 
131  BorderImage {
132  id: appmenuDropShadow
133  anchors {
134  fill: __applicationMenus
135  margins: -units.gu(1)
136  }
137  visible: !__applicationMenus.fullyClosed
138  source: "graphics/rectangular_dropshadow.sci"
139  }
140 
141  BorderImage {
142  id: panelDropShadow
143  anchors {
144  fill: panelAreaBackground
145  bottomMargin: -units.gu(1)
146  }
147  visible: panelState.dropShadow
148  source: "graphics/rectangular_dropshadow.sci"
149  }
150 
151  Rectangle {
152  id: panelAreaBackground
153  color: callHint.visible ? theme.palette.normal.activity : theme.palette.normal.background
154  anchors {
155  top: parent.top
156  left: parent.left
157  right: parent.right
158  }
159  height: minimizedPanelHeight
160 
161  Behavior on color { ColorAnimation { duration: LomiriAnimation.FastDuration } }
162  }
163 
164  MouseArea {
165  id: decorationMouseArea
166  objectName: "windowControlArea"
167  anchors {
168  left: parent.left
169  right: parent.right
170  }
171  height: minimizedPanelHeight
172  hoverEnabled: !__indicators.shown
173  onClicked: {
174  if (callHint.visible) {
175  callHint.showLiveCall();
176  }
177  }
178 
179  onPressed: {
180  if (!callHint.visible) {
181  // let it fall through to the window decoration of the maximized window behind, if any
182  mouse.accepted = false;
183  }
184  var menubar = menuBarLoader.item;
185  if (menubar) {
186  menubar.invokeMenu(mouse);
187  }
188  }
189 
190  Row {
191  anchors.fill: parent
192  spacing: units.gu(2)
193 
194  // WindowControlButtons inside the mouse area, otherwise QML doesn't grok nested hover events :/
195  // cf. https://bugreports.qt.io/browse/QTBUG-32909
196  WindowControlButtons {
197  id: windowControlButtons
198  objectName: "panelWindowControlButtons"
199  height: indicators.minimizedPanelHeight
200  opacity: d.showWindowDecorationControls ? 1 : 0
201  visible: opacity != 0
202  Behavior on opacity { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
203 
204  active: panelState.decorationsVisible || panelState.decorationsAlwaysVisible
205  windowIsMaximized: true
206  onCloseClicked: panelState.closeClicked()
207  onMinimizeClicked: panelState.minimizeClicked()
208  onMaximizeClicked: panelState.restoreClicked()
209  closeButtonShown: panelState.closeButtonShown
210  }
211 
212  Loader {
213  id: menuBarLoader
214  objectName: "menuBarLoader"
215  height: parent.height
216  enabled: d.enablePointerMenu
217  opacity: d.showPointerMenu ? 1 : 0
218  visible: opacity != 0
219  Behavior on opacity { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
220  active: d.showPointerMenu && !callHint.visible
221 
222  width: parent.width - windowControlButtons.width - units.gu(2) - __indicators.barWidth
223 
224  readonly property bool menusRequested: menuBarLoader.item ? menuBarLoader.item.showRequested : false
225 
226  sourceComponent: MenuBar {
227  id: bar
228  objectName: "menuBar"
229  anchors.left: menuBarLoader ? menuBarLoader.left : undefined
230  anchors.margins: units.gu(1)
231  height: menuBarLoader.height
232  enableKeyFilter: valid && panelState.decorationsVisible
233  lomiriMenuModel: __applicationMenus.model
234  panelState: root.panelState
235 
236  Connections {
237  target: __applicationMenus
238  onShownChanged: bar.dismiss();
239  }
240 
241  Connections {
242  target: __indicators
243  onShownChanged: bar.dismiss();
244  }
245 
246  onDoubleClicked: panelState.restoreClicked()
247  onPressed: mouse.accepted = false // let the parent mouse area handle this, so it can both unsnap window and show menu
248  }
249  }
250  }
251 
252  ActiveCallHint {
253  id: callHint
254  objectName: "callHint"
255 
256  anchors.centerIn: parent
257  height: minimizedPanelHeight
258 
259  visible: active && indicators.state == "initial" && __applicationMenus.state == "initial"
260  greeterShown: root.greeterShown
261  }
262  }
263 
264  PanelMenu {
265  id: __applicationMenus
266 
267  x: menuContentX
268  model: registeredMenuModel.model
269  width: root.menuWidth
270  overFlowWidth: width
271  minimizedPanelHeight: root.minimizedPanelHeight
272  expandedPanelHeight: root.expandedPanelHeight
273  openedHeight: root.height
274  alignment: Qt.AlignLeft
275  enableHint: !callHint.active && !fullscreenMode
276  showOnClick: false
277  panelColor: panelAreaBackground.color
278 
279  onShowTapped: {
280  if (callHint.active) {
281  callHint.showLiveCall();
282  }
283  }
284 
285  hideRow: !expanded
286  rowItemDelegate: ActionItem {
287  id: actionItem
288  property int ownIndex: index
289  objectName: "appMenuItem"+index
290  enabled: model.sensitive
291 
292  width: _title.width + units.gu(2)
293  height: parent.height
294 
295  action: Action {
296  text: model.label.replace("_", "&")
297  }
298 
299  Label {
300  id: _title
301  anchors.centerIn: parent
302  text: actionItem.text
303  horizontalAlignment: Text.AlignLeft
304  color: enabled ? theme.palette.normal.backgroundText : theme.palette.disabled.backgroundText
305  }
306  }
307 
308  pageDelegate: PanelMenuPage {
309  readonly property bool isCurrent: modelIndex == __applicationMenus.currentMenuIndex
310  onIsCurrentChanged: {
311  if (isCurrent && menuModel) {
312  menuModel.aboutToShow(modelIndex);
313  }
314  }
315 
316  menuModel: __applicationMenus.model
317  submenuIndex: modelIndex
318 
319  factory: ApplicationMenuItemFactory {
320  rootModel: __applicationMenus.model
321  }
322  }
323 
324  enabled: d.enableTouchMenus
325  opacity: d.showTouchMenu ? 1 : 0
326  visible: opacity != 0
327  Behavior on opacity { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
328 
329  onEnabledChanged: {
330  if (!enabled) hide();
331  }
332  }
333 
334  Item {
335  id: panelTitleHolder
336  anchors {
337  left: parent.left
338  leftMargin: units.gu(1)
339  right: __indicators.left
340  rightMargin: units.gu(1)
341  }
342  height: root.minimizedPanelHeight
343 
344  Label {
345  id: rowLabel
346  anchors {
347  left: parent.left
348  right: root.partialWidth ? parent.right : parent.left
349  rightMargin: touchMenuIcon.width
350  }
351  objectName: "panelTitle"
352  height: root.minimizedPanelHeight
353  verticalAlignment: Text.AlignVCenter
354  elide: Text.ElideRight
355  maximumLineCount: 1
356  fontSize: "medium"
357  font.weight: Font.Medium
358  color: theme.palette.selected.backgroundText
359  text: (root.partialWidth && !callHint.visible) ? panelState.title : ""
360  opacity: __applicationMenus.visible && !__applicationMenus.expanded
361  Behavior on opacity { NumberAnimation { duration: LomiriAnimation.SnapDuration } }
362  visible: opacity !== 0
363  }
364 
365  Icon {
366  id: touchMenuIcon
367  objectName: "touchMenuIcon"
368  anchors {
369  left: parent.left
370  leftMargin: rowLabel.contentWidth + units.dp(2)
371  verticalCenter: parent.verticalCenter
372  }
373  width: units.gu(2)
374  height: units.gu(2)
375  name: "down"
376  color: theme.palette.normal.backgroundText
377  opacity: !__applicationMenus.expanded && d.enableTouchMenus && !callHint.visible
378  Behavior on opacity { NumberAnimation { duration: LomiriAnimation.SnapDuration } }
379  visible: opacity !== 0
380  }
381  }
382 
383  PanelMenu {
384  id: __indicators
385  objectName: "indicators"
386 
387  anchors {
388  top: parent.top
389  right: parent.right
390  }
391  width: root.menuWidth
392  minimizedPanelHeight: root.minimizedPanelHeight
393  expandedPanelHeight: root.expandedPanelHeight
394  openedHeight: root.height
395 
396  overFlowWidth: width - appMenuClear
397  enableHint: !callHint.active && !fullscreenMode
398  showOnClick: !callHint.visible
399  panelColor: panelAreaBackground.color
400 
401  // On small screens, the Indicators' handle area is the entire top
402  // bar unless there is an application menu. In that case, our handle
403  // needs to allow for some room to clear the application menu.
404  property var appMenuClear: (d.enableTouchMenus && !partialWidth) ? units.gu(7) : 0
405 
406  onShowTapped: {
407  if (callHint.active) {
408  callHint.showLiveCall();
409  }
410  }
411 
412  rowItemDelegate: IndicatorItem {
413  id: indicatorItem
414  objectName: identifier+"-panelItem"
415 
416  property int ownIndex: index
417  readonly property bool overflow: parent.width - (x - __indicators.rowContentX) > __indicators.overFlowWidth
418  readonly property bool hidden: !expanded && (overflow || !indicatorVisible || hideSessionIndicator || hideKeyboardIndicator)
419  // HACK for indicator-session
420  readonly property bool hideSessionIndicator: identifier == "indicator-session" && Math.min(Screen.width, Screen.height) <= units.gu(60)
421  // HACK for indicator-keyboard
422  readonly property bool hideKeyboardIndicator: identifier == "indicator-keyboard" && !hasKeyboard
423 
424  height: parent.height
425  expanded: indicators.expanded
426  selected: ListView.isCurrentItem
427 
428  identifier: model.identifier
429  busName: indicatorProperties.busName
430  actionsObjectPath: indicatorProperties.actionsObjectPath
431  menuObjectPath: indicatorProperties.menuObjectPath
432 
433  opacity: hidden ? 0.0 : 1.0
434  Behavior on opacity { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
435 
436  width: ((expanded || indicatorVisible) && !hideSessionIndicator && !hideKeyboardIndicator) ? implicitWidth : 0
437 
438  Behavior on width { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
439  }
440 
441  pageDelegate: PanelMenuPage {
442  objectName: modelData.identifier + "-page"
443  submenuIndex: 0
444 
445  menuModel: delegate.menuModel
446 
447  factory: IndicatorMenuItemFactory {
448  indicator: {
449  var context = modelData.identifier;
450  if (context && context.indexOf("fake-") === 0) {
451  context = context.substring("fake-".length)
452  }
453  return context;
454  }
455  rootModel: delegate.menuModel
456  }
457 
458  IndicatorDelegate {
459  id: delegate
460  busName: modelData.indicatorProperties.busName
461  actionsObjectPath: modelData.indicatorProperties.actionsObjectPath
462  menuObjectPath: modelData.indicatorProperties.menuObjectPath
463  }
464  }
465 
466  enabled: !applicationMenus.expanded
467  opacity: !callHint.visible && !applicationMenus.expanded ? 1 : 0
468  Behavior on opacity { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
469 
470  onEnabledChanged: {
471  if (!enabled) hide();
472  }
473  }
474  }
475 
476  IndicatorsLight {
477  id: indicatorLights
478  supportsMultiColorLed: root.supportsMultiColorLed
479  }
480 
481  states: [
482  State {
483  name: "onscreen" //fully opaque and visible at top edge of screen
484  when: !fullscreenMode
485  PropertyChanges {
486  target: panelArea;
487  anchors.topMargin: 0
488  opacity: 1;
489  }
490  },
491  State {
492  name: "offscreen" //pushed off screen
493  when: fullscreenMode
494  PropertyChanges {
495  target: panelArea;
496  anchors.topMargin: {
497  if (indicators.state !== "initial") return 0;
498  if (applicationMenus.state !== "initial") return 0;
499  return -minimizedPanelHeight;
500  }
501  opacity: indicators.fullyClosed && applicationMenus.fullyClosed ? 0.0 : 1.0
502  }
503  PropertyChanges {
504  target: indicators.showDragHandle;
505  anchors.bottomMargin: -units.gu(1)
506  }
507  PropertyChanges {
508  target: applicationMenus.showDragHandle;
509  anchors.bottomMargin: -units.gu(1)
510  }
511  }
512  ]
513 
514  transitions: [
515  Transition {
516  to: "onscreen"
517  LomiriNumberAnimation { target: panelArea; properties: "anchors.topMargin,opacity" }
518  },
519  Transition {
520  to: "offscreen"
521  LomiriNumberAnimation { target: panelArea; properties: "anchors.topMargin,opacity" }
522  }
523  ]
524 }