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