Unity 8
DashPageHeader.qml
1 /*
2  * Copyright (C) 2013,2015,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 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 General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.4
18 import Ubuntu.Components 1.3
19 import Ubuntu.Components.Popups 1.3
20 import Ubuntu.Components.ListItems 1.3
21 import Utils 0.1
22 import "../Components"
23 
24 Item {
25  id: root
26  objectName: "pageHeader"
27  implicitHeight: headerContainer.height + signatureLineHeight
28  readonly property real signatureLineHeight: showSignatureLine ? units.gu(2) : 0
29  readonly property real headerDividerLuminance: Style.luminance(bottomBorder.color)
30 
31  property int activeFiltersCount: 0
32  property bool scopeHasFilters: false
33  property bool showBackButton: false
34  property bool backIsClose: false
35  property string title
36  property var extraPanel
37  property string navigationTag
38 
39  property bool storeEntryEnabled: false
40  property bool searchEntryEnabled: false
41  property bool settingsEnabled: false
42  property bool favoriteEnabled: false
43  property bool favorite: false
44  property ListModel searchHistory
45  property alias searchQuery: searchTextField.text
46  property alias searchHint: searchTextField.placeholderText
47  property bool showSignatureLine: true
48 
49  property int paginationCount: 0
50  property int paginationIndex: -1
51 
52  property var scopeStyle: null
53 
54  signal clearSearch(bool keepPanelOpen)
55  signal backClicked()
56  signal storeClicked()
57  signal settingsClicked()
58  signal favoriteClicked()
59  signal searchTextFieldFocused()
60  signal showFiltersPopup(var item)
61 
62  onScopeStyleChanged: refreshLogo()
63  onSearchQueryChanged: {
64  // Make sure we are at the search page if the search query changes behind our feet
65  if (searchQuery) {
66  headerContainer.showSearch = true;
67  }
68  }
69  onNavigationTagChanged: {
70  // Make sure we are at the search page if the navigation tag changes behind our feet
71  if (navigationTag) {
72  headerContainer.showSearch = true;
73  }
74  }
75 
76  function triggerSearch() {
77  if (searchEntryEnabled) {
78  headerContainer.showSearch = true;
79  searchTextField.forceActiveFocus();
80  }
81  }
82 
83  function closePopup(keepFocus, keepSearch) {
84  if (extraPanel.visible) {
85  extraPanel.visible = false;
86  }
87  if (!keepFocus) {
88  unfocus(keepSearch);
89  }
90  if (!keepSearch && !searchTextField.text && !root.navigationTag && searchHistory.count == 0) {
91  headerContainer.showSearch = false;
92  }
93  }
94 
95  function resetSearch(keepFocus) {
96  if (searchHistory) {
97  searchHistory.addQuery(searchTextField.text);
98  }
99  searchTextField.text = "";
100  closePopup(keepFocus);
101  }
102 
103  function unfocus(keepSearch) {
104  searchTextField.focus = false;
105  if (!keepSearch && !searchTextField.text && !root.navigationTag) {
106  headerContainer.showSearch = false;
107  }
108  }
109 
110  function openPopup() {
111  if (openSearchAnimation.running) {
112  openSearchAnimation.openPopup = true;
113  } else if (extraPanel.hasContents) {
114  // Show extraPanel
115  extraPanel.visible = true;
116  }
117  }
118 
119  function refreshLogo() {
120  if (root.scopeStyle ? root.scopeStyle.headerLogo != "" : false) {
121  header.contents = imageComponent.createObject();
122  } else if (header.contents) {
123  header.contents.destroy();
124  header.contents = null;
125  }
126  }
127 
128  Connections {
129  target: root.scopeStyle
130  onHeaderLogoChanged: root.refreshLogo()
131  }
132 
133  InverseMouseArea {
134  anchors { fill: parent; margins: units.gu(1); bottomMargin: units.gu(3) + (extraPanel ? extraPanel.height : 0) }
135  visible: headerContainer.showSearch
136  onPressed: {
137  closePopup(/* keepFocus */false);
138  mouse.accepted = false;
139  }
140  }
141 
142  Item {
143  id: headerContainer
144  objectName: "headerContainer"
145  anchors { left: parent.left; top: parent.top; right: parent.right }
146  height: header.__styleInstance.contentHeight
147 
148  property bool showSearch: false
149 
150  state: headerContainer.showSearch ? "search" : ""
151 
152  states: State {
153  name: "search"
154 
155  AnchorChanges {
156  target: headersColumn
157  anchors.top: parent.top
158  anchors.bottom: undefined
159  }
160  }
161 
162  transitions: Transition {
163  id: openSearchAnimation
164  AnchorAnimation {
165  duration: UbuntuAnimation.FastDuration
166  easing: UbuntuAnimation.StandardEasing
167  }
168 
169  property bool openPopup: false
170 
171  onRunningChanged: {
172  headerContainer.clip = running;
173  if (!running && openSearchAnimation.openPopup) {
174  openSearchAnimation.openPopup = false;
175  root.openPopup();
176  }
177  }
178  }
179 
180  Background {
181  id: background
182  objectName: "headerBackground"
183  style: scopeStyle.headerBackground
184  }
185 
186  Column {
187  id: headersColumn
188  anchors {
189  left: parent.left
190  right: parent.right
191  bottom: parent.bottom
192  }
193 
194  PageHeader {
195  id: searchHeader
196  anchors { left: parent.left; right: parent.right }
197  opacity: headerContainer.clip || headerContainer.showSearch ? 1 : 0 // setting visible false cause column to relayout
198 
199  StyleHints {
200  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
201  backgroundColor: "transparent"
202  dividerColor: "transparent"
203  }
204 
205  contents: Item {
206  anchors.fill: parent
207 
208  Keys.onEscapePressed: { // clear the search text, dismiss the search in the second step
209  if (searchTextField.text != "") {
210  root.clearSearch(true);
211  } else {
212  root.clearSearch(false);
213  headerContainer.showSearch = false;
214  }
215  }
216 
217  TextField {
218  id: searchTextField
219  objectName: "searchTextField"
220  inputMethodHints: Qt.ImhNoPredictiveText
221  hasClearButton: false
222  anchors {
223  top: parent.top
224  topMargin: units.gu(1)
225  left: parent.left
226  bottom: parent.bottom
227  bottomMargin: units.gu(1)
228  right: settingsButton.left
229  rightMargin: settingsButton.visible ? 0 : units.gu(2)
230  }
231 
232  primaryItem: Rectangle {
233  color: "#F5F4F5"
234  width: root.navigationTag != "" ? tagLabel.width + units.gu(2) : 0
235  height: root.navigationTag != "" ? tagLabel.height + units.gu(1) : 0
236  radius: units.gu(0.5)
237  Label {
238  id: tagLabel
239  text: root.navigationTag
240  anchors.centerIn: parent
241  color: "#333333"
242  }
243  }
244 
245  secondaryItem: AbstractButton {
246  id: clearButton
247  height: searchTextField.height
248  width: height
249  enabled: searchTextField.text.length > 0 || root.navigationTag != ""
250 
251  Image {
252  objectName: "clearIcon"
253  anchors.fill: parent
254  anchors.margins: units.gu(1)
255  source: "image://theme/clear"
256  sourceSize.width: width
257  sourceSize.height: height
258  opacity: parent.enabled
259  visible: opacity > 0
260  Behavior on opacity {
261  UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
262  }
263  }
264 
265  onClicked: {
266  root.clearSearch(true);
267  }
268  }
269 
270  onActiveFocusChanged: {
271  if (activeFocus) {
272  root.searchTextFieldFocused();
273  root.openPopup();
274  }
275  }
276 
277  onTextChanged: {
278  if (text != "") {
279  closePopup(/* keepFocus */true);
280  }
281  }
282  }
283 
284  AbstractButton {
285  id: settingsButton
286  objectName: "settingsButton"
287 
288  width: root.scopeHasFilters ? height : 0
289  visible: width > 0
290  anchors {
291  top: parent.top
292  right: cancelButton.left
293  bottom: parent.bottom
294  rightMargin: units.gu(-1)
295  }
296 
297  Icon {
298  anchors.fill: parent
299  anchors.margins: units.gu(2)
300  name: "filters"
301  color: root.activeFiltersCount > 0 ? theme.palette.normal.positive : header.__styleInstance.foregroundColor
302  }
303 
304  onClicked: {
305  root.showFiltersPopup(settingsButton);
306  }
307  }
308 
309  AbstractButton {
310  id: cancelButton
311  objectName: "cancelButton"
312  width: cancelLabel.width + cancelLabel.anchors.rightMargin + cancelLabel.anchors.leftMargin
313  anchors {
314  top: parent.top
315  right: parent.right
316  bottom: parent.bottom
317  }
318  onClicked: {
319  root.clearSearch(false);
320  headerContainer.showSearch = false;
321  }
322  Label {
323  id: cancelLabel
324  text: i18n.tr("Cancel")
325  color: header.__styleInstance.foregroundColor
326  verticalAlignment: Text.AlignVCenter
327  anchors {
328  verticalCenter: parent.verticalCenter
329  right: parent.right
330  rightMargin: units.gu(2)
331  leftMargin: units.gu(1)
332  }
333  }
334  }
335  }
336  }
337 
338  PageHeader {
339  id: header
340  objectName: "innerPageHeader"
341  anchors { left: parent.left; right: parent.right }
342  height: headerContainer.height
343  opacity: headerContainer.clip || !headerContainer.showSearch ? 1 : 0 // setting visible false cause column to relayout
344  title: root.title
345 
346  StyleHints {
347  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
348  backgroundColor: "transparent"
349  dividerColor: "transparent"
350  }
351 
352  leadingActionBar.actions: Action {
353  iconName: backIsClose ? "close" : "back"
354  visible: root.showBackButton
355  onTriggered: root.backClicked()
356  }
357 
358  trailingActionBar {
359  actions: [
360  Action {
361  objectName: "store"
362  text: i18n.ctr("Button: Open the Ubuntu Store", "Store")
363  iconName: "ubuntu-store-symbolic"
364  visible: root.storeEntryEnabled
365  onTriggered: root.storeClicked();
366  },
367  Action {
368  objectName: "search"
369  text: i18n.ctr("Button: Start a search in the current dash scope", "Search")
370  iconName: "search"
371  visible: root.searchEntryEnabled
372  onTriggered: {
373  headerContainer.showSearch = true;
374  searchTextField.forceActiveFocus();
375  }
376  },
377  Action {
378  objectName: "settings"
379  text: i18n.ctr("Button: Show the current dash scope settings", "Settings")
380  iconName: "settings"
381  visible: root.settingsEnabled
382  onTriggered: root.settingsClicked()
383  },
384  Action {
385  objectName: "favorite"
386  text: root.favorite ? i18n.tr("Remove from Favorites") : i18n.tr("Add to Favorites")
387  iconName: root.favorite ? "starred" : "non-starred"
388  visible: root.favoriteEnabled
389  onTriggered: root.favoriteClicked()
390  }
391  ]
392  }
393 
394  Component.onCompleted: root.refreshLogo()
395 
396  Component {
397  id: imageComponent
398 
399  Item {
400  anchors { fill: parent; topMargin: units.gu(1.5); bottomMargin: units.gu(1.5) }
401  clip: true
402  Image {
403  objectName: "titleImage"
404  anchors.fill: parent
405  source: root.scopeStyle ? root.scopeStyle.headerLogo : ""
406  fillMode: Image.PreserveAspectFit
407  horizontalAlignment: Image.AlignLeft
408  sourceSize.height: height
409  }
410  }
411  }
412  }
413  }
414  }
415 
416  Rectangle {
417  id: bottomBorder
418  visible: showSignatureLine
419  anchors {
420  top: headerContainer.bottom
421  left: parent.left
422  right: parent.right
423  bottom: parent.bottom
424  }
425 
426  color: root.scopeStyle ? root.scopeStyle.headerDividerColor : "#e0e0e0"
427 
428  Rectangle {
429  anchors {
430  top: parent.top
431  left: parent.left
432  right: parent.right
433  }
434  height: units.dp(1)
435  color: Qt.darker(parent.color, 1.1)
436  }
437  }
438 
439  Row {
440  visible: bottomBorder.visible
441  spacing: units.gu(.5)
442  Repeater {
443  objectName: "paginationRepeater"
444  model: root.paginationCount
445  Image {
446  objectName: "paginationDots_" + index
447  height: units.gu(1)
448  width: height
449  source: (index == root.paginationIndex) ? "graphics/pagination_dot_on.png" : "graphics/pagination_dot_off.png"
450  }
451  }
452  anchors {
453  top: headerContainer.bottom
454  horizontalCenter: headerContainer.horizontalCenter
455  topMargin: units.gu(.5)
456  }
457  }
458 
459  // FIXME this doesn't work with solid scope backgrounds due to z-ordering
460  Item {
461  id: bottomHighlight
462  visible: bottomBorder.visible
463  anchors {
464  top: parent.bottom
465  left: parent.left
466  right: parent.right
467  }
468  z: 1
469  height: units.dp(1)
470  opacity: 0.6
471 
472  Rectangle {
473  anchors.fill: parent
474  color: if (root.scopeStyle) {
475  Qt.lighter(Qt.rgba(root.scopeStyle.background.r,
476  root.scopeStyle.background.g,
477  root.scopeStyle.background.b, 1.0), 1.2);
478  } else "#CCFFFFFF"
479  }
480  }
481 }