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