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