Unity 8
PageHeader.qml
1 /*
2  * Copyright (C) 2013 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.2
18 import Ubuntu.Components 1.1
19 import Ubuntu.Components.Themes.Ambiance 1.1
20 import Ubuntu.Components.Popups 1.0
21 import Ubuntu.Components.ListItems 1.0
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  }
106  }
107 
108  function refreshLogo() {
109  if (root.scopeStyle ? root.scopeStyle.headerLogo != "" : false) {
110  header.contents = imageComponent.createObject();
111  } else if (header.contents) {
112  header.contents.destroy();
113  header.contents = null;
114  }
115  }
116 
117  Connections {
118  target: root.scopeStyle
119  onHeaderLogoChanged: root.refreshLogo()
120  }
121 
122  InverseMouseArea {
123  anchors { fill: parent; margins: units.gu(1); bottomMargin: units.gu(3) + bottomContainer.height }
124  visible: headerContainer.showSearch
125  onPressed: {
126  closePopup(/* keepFocus */false);
127  if (!searchTextField.text) {
128  headerContainer.showSearch = false;
129  }
130  mouse.accepted = false;
131  }
132  }
133 
134  Flickable {
135  id: headerContainer
136  objectName: "headerContainer"
137  clip: contentY < height
138  anchors { left: parent.left; top: parent.top; right: parent.right }
139  height: units.gu(7)
140  contentHeight: headersColumn.height
141  interactive: false
142  contentY: showSearch ? 0 : height
143 
144  property bool showSearch: false
145  property var popover: null
146 
147  Background {
148  id: background
149  objectName: "headerBackground"
150  style: scopeStyle.headerBackground
151  }
152 
153  Behavior on contentY {
154  UbuntuNumberAnimation {
155  id: openSearchAnimation
156  property bool openSearchHistory: false
157 
158  onRunningChanged: {
159  if (!running && openSearchAnimation.openSearchHistory) {
160  openSearchAnimation.openSearchHistory = false;
161  root.openSearchHistory();
162  }
163  }
164  }
165  }
166 
167  Column {
168  id: headersColumn
169  anchors { left: parent.left; right: parent.right }
170 
171  PageHeadStyle {
172  id: searchHeader
173  anchors { left: parent.left; right: parent.right }
174  height: headerContainer.height
175  contentHeight: height
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 string title
181  property color dividerColor: "transparent" // Doesn't matter as we don't have PageHeadSections
182  property color panelColor: background.topColor
183  panelForegroundColor: config.foregroundColor
184  property var 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  id: header
247  objectName: "innerPageHeader"
248  anchors { left: parent.left; right: parent.right }
249  height: headerContainer.height
250  contentHeight: 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 string title: root.title
255  property color dividerColor: "transparent" // Doesn't matter as we don't have PageHeadSections
256  property color panelColor: background.topColor
257  panelForegroundColor: config.foregroundColor
258  property var config: PageHeadConfiguration {
259  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : Theme.palette.normal.baseText
260  backAction: Action {
261  iconName: backIsClose ? "close" : "back"
262  visible: root.showBackButton
263  onTriggered: root.backClicked()
264  }
265 
266  actions: [
267  Action {
268  objectName: "store"
269  text: i18n.tr("Store")
270  iconName: "ubuntu-store-symbolic"
271  visible: root.storeEntryEnabled
272  onTriggered: root.storeClicked();
273  },
274  Action {
275  objectName: "search"
276  text: i18n.tr("Search")
277  iconName: "search"
278  visible: root.searchEntryEnabled
279  onTriggered: {
280  headerContainer.showSearch = true;
281  searchTextField.forceActiveFocus();
282  }
283  },
284  Action {
285  objectName: "settings"
286  text: i18n.tr("Settings")
287  iconName: "settings"
288  visible: root.settingsEnabled
289  onTriggered: root.settingsClicked()
290  },
291  Action {
292  objectName: "favorite"
293  text: root.favorite ? i18n.tr("Remove from Favorites") : i18n.tr("Add to Favorites")
294  iconName: root.favorite ? "starred" : "non-starred"
295  visible: root.favoriteEnabled
296  onTriggered: root.favoriteClicked()
297  }
298  ]
299  }
300 
301  property var contents: null
302  Component.onCompleted: root.refreshLogo()
303 
304  Component {
305  id: imageComponent
306 
307  Item {
308  anchors { fill: parent; topMargin: units.gu(1.5); bottomMargin: units.gu(1.5) }
309  clip: true
310  Image {
311  objectName: "titleImage"
312  anchors.fill: parent
313  source: root.scopeStyle ? root.scopeStyle.headerLogo : ""
314  fillMode: Image.PreserveAspectFit
315  horizontalAlignment: Image.AlignLeft
316  sourceSize.height: height
317  }
318  }
319  }
320  }
321  }
322  }
323 
324  Component {
325  id: popoverComponent
326  Popover {
327  id: popover
328  autoClose: false
329 
330  property bool unfocusOnDestruction: false
331 
332  Component.onDestruction: {
333  headerContainer.popover = null;
334  if (unfocusOnDestruction) {
335  root.unfocus();
336  }
337  }
338 
339  Column {
340  anchors {
341  top: parent.top
342  left: parent.left
343  right: parent.right
344  }
345 
346  Repeater {
347  id: recentSearches
348  objectName: "recentSearches"
349  model: searchHistory
350 
351  delegate: Standard {
352  showDivider: index < recentSearches.count - 1
353  text: query
354  onClicked: {
355  searchHistory.addQuery(text);
356  searchTextField.text = text;
357  closePopup(/* keepFocus */false);
358  }
359  }
360  }
361  }
362  }
363  }
364 
365  Rectangle {
366  id: bottomBorder
367  visible: showSignatureLine
368  anchors {
369  top: headerContainer.bottom
370  left: parent.left
371  right: parent.right
372  bottom: bottomContainer.top
373  }
374 
375  color: root.scopeStyle ? root.scopeStyle.headerDividerColor : "#e0e0e0"
376 
377  Rectangle {
378  anchors {
379  top: parent.top
380  left: parent.left
381  right: parent.right
382  }
383  height: units.dp(1)
384  color: Qt.darker(parent.color, 1.1)
385  }
386  }
387 
388  Row {
389  visible: bottomBorder.visible
390  spacing: units.gu(.5)
391  Repeater {
392  objectName: "paginationRepeater"
393  model: root.paginationCount
394  Image {
395  objectName: "paginationDots_" + index
396  height: units.gu(1)
397  width: height
398  source: (index == root.paginationIndex) ? "graphics/pagination_dot_on.png" : "graphics/pagination_dot_off.png"
399  }
400  }
401  anchors {
402  top: headerContainer.bottom
403  horizontalCenter: headerContainer.horizontalCenter
404  topMargin: units.gu(.5)
405  }
406  }
407 
408  // FIXME this doesn't work with solid scope backgrounds due to z-ordering
409  Item {
410  id: bottomHighlight
411  visible: bottomBorder.visible
412  anchors {
413  top: bottomContainer.top
414  left: parent.left
415  right: parent.right
416  }
417  z: 1
418  height: units.dp(1)
419  opacity: 0.6
420 
421  // FIXME this should be a shader when bottomItem exists
422  // to support image backgrounds
423  Rectangle {
424  anchors.fill: parent
425  color: if (bottomItem && bottomItem.background) {
426  Qt.lighter(Qt.rgba(bottomItem.background.topColor.r,
427  bottomItem.background.topColor.g,
428  bottomItem.background.topColor.b, 1.0), 1.2);
429  } else if (!bottomItem && root.scopeStyle) {
430  Qt.lighter(Qt.rgba(root.scopeStyle.background.r,
431  root.scopeStyle.background.g,
432  root.scopeStyle.background.b, 1.0), 1.2);
433  } else "#CCFFFFFF"
434  }
435  }
436 
437  Item {
438  id: bottomContainer
439 
440  anchors {
441  left: parent.left
442  right: parent.right
443  bottom: parent.bottom
444  }
445  height: childrenRect.height
446  }
447 }