Unity 8
DashPageHeader.qml
1 /*
2  * Copyright (C) 2013,2015 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.Themes.Ambiance 1.3
20 import Ubuntu.Components.Popups 1.3
21 import Ubuntu.Components.ListItems 1.3
22 import "../Components"
23 import "../Components/SearchHistoryModel"
24 
25 Item {
26  id: root
27  objectName: "pageHeader"
28  implicitHeight: headerContainer.height + bottomContainer.height + (showSignatureLine ? units.gu(2) : 0)
29 
30  property bool showBackButton: false
31  property bool backIsClose: false
32  property string title
33 
34  property bool storeEntryEnabled: false
35  property bool searchEntryEnabled: false
36  property bool settingsEnabled: false
37  property bool favoriteEnabled: false
38  property bool favorite: false
39  property ListModel searchHistory: SearchHistoryModel
40  property alias searchQuery: searchTextField.text
41  property alias searchHint: searchTextField.placeholderText
42  property bool showSignatureLine: true
43 
44  property alias bottomItem: bottomContainer.children
45  property int paginationCount: 0
46  property int paginationIndex: -1
47 
48  property var scopeStyle: null
49 
50  signal backClicked()
51  signal storeClicked()
52  signal settingsClicked()
53  signal favoriteClicked()
54  signal searchTextFieldFocused()
55 
56  onScopeStyleChanged: refreshLogo()
57  onSearchQueryChanged: {
58  // Make sure we are at the search page if the search query changes behind our feet
59  if (searchQuery) {
60  headerContainer.showSearch = true;
61  }
62  }
63 
64  function triggerSearch() {
65  if (searchEntryEnabled) {
66  headerContainer.showSearch = true;
67  searchTextField.forceActiveFocus();
68  }
69  }
70 
71  function closePopup(keepFocus) {
72  if (headerContainer.popover != null) {
73  headerContainer.popover.unfocusOnDestruction = !keepFocus;
74  PopupUtils.close(headerContainer.popover);
75  } else if (!keepFocus) {
76  unfocus();
77  }
78  }
79 
80  function resetSearch(keepFocus) {
81  if (searchHistory) {
82  searchHistory.addQuery(searchTextField.text);
83  }
84  searchTextField.text = "";
85  closePopup(keepFocus);
86  }
87 
88  function unfocus() {
89  searchTextField.focus = false;
90  if (!searchTextField.text) {
91  headerContainer.showSearch = false;
92  }
93  }
94 
95  function openSearchHistory() {
96  if (openSearchAnimation.running) {
97  openSearchAnimation.openSearchHistory = true;
98  } else if (root.searchHistory.count > 0 && headerContainer.popover == null) {
99  headerContainer.popover = PopupUtils.open(popoverComponent, searchTextField,
100  {
101  "contentWidth": searchTextField.width,
102  "edgeMargins": units.gu(1)
103  }
104  );
105  searchTextField.forceActiveFocus();
106  }
107  }
108 
109  function refreshLogo() {
110  if (root.scopeStyle ? root.scopeStyle.headerLogo != "" : false) {
111  header.contents = imageComponent.createObject();
112  } else if (header.contents) {
113  header.contents.destroy();
114  header.contents = null;
115  }
116  }
117 
118  Connections {
119  target: root.scopeStyle
120  onHeaderLogoChanged: root.refreshLogo()
121  }
122 
123  InverseMouseArea {
124  anchors { fill: parent; margins: units.gu(1); bottomMargin: units.gu(3) + bottomContainer.height }
125  visible: headerContainer.showSearch
126  onPressed: {
127  closePopup(/* keepFocus */false);
128  if (!searchTextField.text) {
129  headerContainer.showSearch = false;
130  }
131  mouse.accepted = false;
132  }
133  }
134 
135  Flickable {
136  id: headerContainer
137  objectName: "headerContainer"
138  clip: contentY < height
139  anchors { left: parent.left; top: parent.top; right: parent.right }
140  height: header.contentHeight
141  contentHeight: headersColumn.height
142  interactive: false
143  contentY: showSearch ? 0 : height
144 
145  property bool showSearch: false
146  property var popover: null
147 
148  Background {
149  id: background
150  objectName: "headerBackground"
151  style: scopeStyle.headerBackground
152  }
153 
154  Behavior on contentY {
155  UbuntuNumberAnimation {
156  id: openSearchAnimation
157  property bool openSearchHistory: false
158 
159  onRunningChanged: {
160  if (!running && openSearchAnimation.openSearchHistory) {
161  openSearchAnimation.openSearchHistory = false;
162  root.openSearchHistory();
163  }
164  }
165  }
166  }
167 
168  Column {
169  id: headersColumn
170  anchors { left: parent.left; right: parent.right }
171 
172  PageHeadStyle {
173  // FIXME: Replace PageHeadStyle from the Ambiance theme by the new PageHeader from Ubuntu.Components 1.3.
174  id: searchHeader
175  anchors { left: parent.left; right: parent.right }
176  opacity: headerContainer.clip || headerContainer.showSearch ? 1 : 0 // setting visible false cause column to relayout
177  __separator_visible: false
178  // Required to keep PageHeadStyle noise down as it expects the Page's properties around.
179  property var styledItem: searchHeader
180  property color dividerColor: "transparent" // Doesn't matter as we don't have PageHeadSections
181  property color panelColor: background.topColor
182  panelForegroundColor: config.foregroundColor
183  backgroundColor: "transparent"
184  config: PageHeadConfiguration {
185  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
186  backAction: Action {
187  iconName: "back"
188  onTriggered: {
189  root.resetSearch();
190  headerContainer.showSearch = false;
191  }
192  }
193  }
194  property var contents: TextField {
195  id: searchTextField
196  objectName: "searchTextField"
197  inputMethodHints: Qt.ImhNoPredictiveText
198  hasClearButton: false
199  anchors {
200  fill: parent
201  leftMargin: units.gu(1)
202  topMargin: units.gu(1)
203  bottomMargin: units.gu(1)
204  rightMargin: root.width > units.gu(60) ? root.width - units.gu(40) : units.gu(1)
205  }
206 
207  secondaryItem: AbstractButton {
208  height: searchTextField.height
209  width: height
210  enabled: searchTextField.text.length > 0
211 
212  Image {
213  objectName: "clearIcon"
214  anchors.fill: parent
215  anchors.margins: units.gu(.75)
216  source: "image://theme/clear"
217  opacity: searchTextField.text.length > 0
218  visible: opacity > 0
219  Behavior on opacity {
220  UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
221  }
222  }
223 
224  onClicked: {
225  root.resetSearch(true);
226  root.openSearchHistory();
227  }
228  }
229 
230  onActiveFocusChanged: {
231  if (activeFocus) {
232  root.searchTextFieldFocused();
233  root.openSearchHistory();
234  }
235  }
236 
237  onTextChanged: {
238  if (text != "") {
239  closePopup(/* keepFocus */true);
240  }
241  }
242  }
243  }
244 
245  PageHeadStyle {
246  // FIXME: Replace PageHeadStyle from the Ambiance theme by the new PageHeader from Ubuntu.Components 1.3.
247  id: header
248  objectName: "innerPageHeader"
249  anchors { left: parent.left; right: parent.right }
250  height: headerContainer.height
251  opacity: headerContainer.clip || !headerContainer.showSearch ? 1 : 0 // setting visible false cause column to relayout
252  __separator_visible: false
253  property var styledItem: header
254  property color dividerColor: "transparent" // Doesn't matter as we don't have PageHeadSections
255  property color panelColor: background.topColor
256  panelForegroundColor: config.foregroundColor
257  backgroundColor: "transparent"
258  config: PageHeadConfiguration {
259  title: root.title
260  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
261  backAction: Action {
262  iconName: backIsClose ? "close" : "back"
263  visible: root.showBackButton
264  onTriggered: root.backClicked()
265  }
266 
267  actions: [
268  Action {
269  objectName: "store"
270  text: i18n.ctr("Button: Open the Ubuntu Store", "Store")
271  iconName: "ubuntu-store-symbolic"
272  visible: root.storeEntryEnabled
273  onTriggered: root.storeClicked();
274  },
275  Action {
276  objectName: "search"
277  text: i18n.ctr("Button: Start a search in the current dash scope", "Search")
278  iconName: "search"
279  visible: root.searchEntryEnabled
280  onTriggered: {
281  headerContainer.showSearch = true;
282  searchTextField.forceActiveFocus();
283  }
284  },
285  Action {
286  objectName: "settings"
287  text: i18n.ctr("Button: Show the current dash scope settings", "Settings")
288  iconName: "settings"
289  visible: root.settingsEnabled
290  onTriggered: root.settingsClicked()
291  },
292  Action {
293  objectName: "favorite"
294  text: root.favorite ? i18n.tr("Remove from Favorites") : i18n.tr("Add to Favorites")
295  iconName: root.favorite ? "starred" : "non-starred"
296  visible: root.favoriteEnabled
297  onTriggered: root.favoriteClicked()
298  }
299  ]
300  }
301 
302  property var contents: null
303  Component.onCompleted: root.refreshLogo()
304 
305  Component {
306  id: imageComponent
307 
308  Item {
309  anchors { fill: parent; topMargin: units.gu(1.5); bottomMargin: units.gu(1.5) }
310  clip: true
311  Image {
312  objectName: "titleImage"
313  anchors.fill: parent
314  source: root.scopeStyle ? root.scopeStyle.headerLogo : ""
315  fillMode: Image.PreserveAspectFit
316  horizontalAlignment: Image.AlignLeft
317  sourceSize.height: height
318  }
319  }
320  }
321  }
322  }
323  }
324 
325  Component {
326  id: popoverComponent
327  Popover {
328  id: popover
329  autoClose: false
330 
331  property bool unfocusOnDestruction: false
332 
333  Component.onDestruction: {
334  headerContainer.popover = null;
335  if (unfocusOnDestruction) {
336  root.unfocus();
337  }
338  }
339 
340  Column {
341  anchors {
342  top: parent.top
343  left: parent.left
344  right: parent.right
345  }
346 
347  Repeater {
348  id: recentSearches
349  objectName: "recentSearches"
350  model: searchHistory
351 
352  delegate: Standard {
353  showDivider: index < recentSearches.count - 1
354  text: query
355  onClicked: {
356  searchHistory.addQuery(text);
357  searchTextField.text = text;
358  closePopup(/* keepFocus */false);
359  }
360  }
361  }
362  }
363  }
364  }
365 
366  Rectangle {
367  id: bottomBorder
368  visible: showSignatureLine
369  anchors {
370  top: headerContainer.bottom
371  left: parent.left
372  right: parent.right
373  bottom: bottomContainer.top
374  }
375 
376  color: root.scopeStyle ? root.scopeStyle.headerDividerColor : "#e0e0e0"
377 
378  Rectangle {
379  anchors {
380  top: parent.top
381  left: parent.left
382  right: parent.right
383  }
384  height: units.dp(1)
385  color: Qt.darker(parent.color, 1.1)
386  }
387  }
388 
389  Row {
390  visible: bottomBorder.visible
391  spacing: units.gu(.5)
392  Repeater {
393  objectName: "paginationRepeater"
394  model: root.paginationCount
395  Image {
396  objectName: "paginationDots_" + index
397  height: units.gu(1)
398  width: height
399  source: (index == root.paginationIndex) ? "graphics/pagination_dot_on.png" : "graphics/pagination_dot_off.png"
400  }
401  }
402  anchors {
403  top: headerContainer.bottom
404  horizontalCenter: headerContainer.horizontalCenter
405  topMargin: units.gu(.5)
406  }
407  }
408 
409  // FIXME this doesn't work with solid scope backgrounds due to z-ordering
410  Item {
411  id: bottomHighlight
412  visible: bottomBorder.visible
413  anchors {
414  top: bottomContainer.top
415  left: parent.left
416  right: parent.right
417  }
418  z: 1
419  height: units.dp(1)
420  opacity: 0.6
421 
422  // FIXME this should be a shader when bottomItem exists
423  // to support image backgrounds
424  Rectangle {
425  anchors.fill: parent
426  color: if (bottomItem && bottomItem.background) {
427  Qt.lighter(Qt.rgba(bottomItem.background.topColor.r,
428  bottomItem.background.topColor.g,
429  bottomItem.background.topColor.b, 1.0), 1.2);
430  } else if (!bottomItem && root.scopeStyle) {
431  Qt.lighter(Qt.rgba(root.scopeStyle.background.r,
432  root.scopeStyle.background.g,
433  root.scopeStyle.background.b, 1.0), 1.2);
434  } else "#CCFFFFFF"
435  }
436  }
437 
438  Item {
439  id: bottomContainer
440 
441  anchors {
442  left: parent.left
443  right: parent.right
444  bottom: parent.bottom
445  }
446  height: childrenRect.height
447  }
448 }