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  id: searchHeader
174  anchors { left: parent.left; right: parent.right }
175  opacity: headerContainer.clip || headerContainer.showSearch ? 1 : 0 // setting visible false cause column to relayout
176  __separator_visible: false
177  // Required to keep PageHeadStyle noise down as it expects the Page's properties around.
178  property var styledItem: searchHeader
179  property color dividerColor: "transparent" // Doesn't matter as we don't have PageHeadSections
180  property color panelColor: background.topColor
181  panelForegroundColor: config.foregroundColor
182  config: PageHeadConfiguration {
183  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
184  backAction: Action {
185  iconName: "back"
186  onTriggered: {
187  root.resetSearch();
188  headerContainer.showSearch = false;
189  }
190  }
191  }
192  property var contents: TextField {
193  id: searchTextField
194  objectName: "searchTextField"
195  inputMethodHints: Qt.ImhNoPredictiveText
196  hasClearButton: false
197  anchors {
198  fill: parent
199  leftMargin: units.gu(1)
200  topMargin: units.gu(1)
201  bottomMargin: units.gu(1)
202  rightMargin: root.width > units.gu(60) ? root.width - units.gu(40) : units.gu(1)
203  }
204 
205  secondaryItem: AbstractButton {
206  height: searchTextField.height
207  width: height
208  enabled: searchTextField.text.length > 0
209 
210  Image {
211  objectName: "clearIcon"
212  anchors.fill: parent
213  anchors.margins: units.gu(.75)
214  source: "image://theme/clear"
215  opacity: searchTextField.text.length > 0
216  visible: opacity > 0
217  Behavior on opacity {
218  UbuntuNumberAnimation { duration: UbuntuAnimation.FastDuration }
219  }
220  }
221 
222  onClicked: {
223  root.resetSearch(true);
224  root.openSearchHistory();
225  }
226  }
227 
228  onActiveFocusChanged: {
229  if (activeFocus) {
230  root.searchTextFieldFocused();
231  root.openSearchHistory();
232  }
233  }
234 
235  onTextChanged: {
236  if (text != "") {
237  closePopup(/* keepFocus */true);
238  }
239  }
240  }
241  }
242 
243  PageHeadStyle {
244  id: header
245  objectName: "innerPageHeader"
246  anchors { left: parent.left; right: parent.right }
247  height: headerContainer.height
248  opacity: headerContainer.clip || !headerContainer.showSearch ? 1 : 0 // setting visible false cause column to relayout
249  __separator_visible: false
250  property var styledItem: header
251  property color dividerColor: "transparent" // Doesn't matter as we don't have PageHeadSections
252  property color panelColor: background.topColor
253  panelForegroundColor: config.foregroundColor
254  config: PageHeadConfiguration {
255  title: root.title
256  foregroundColor: root.scopeStyle ? root.scopeStyle.headerForeground : theme.palette.normal.baseText
257  backAction: Action {
258  iconName: backIsClose ? "close" : "back"
259  visible: root.showBackButton
260  onTriggered: root.backClicked()
261  }
262 
263  actions: [
264  Action {
265  objectName: "store"
266  text: i18n.ctr("Button: Open the Ubuntu Store", "Store")
267  iconName: "ubuntu-store-symbolic"
268  visible: root.storeEntryEnabled
269  onTriggered: root.storeClicked();
270  },
271  Action {
272  objectName: "search"
273  text: i18n.ctr("Button: Start a search in the current dash scope", "Search")
274  iconName: "search"
275  visible: root.searchEntryEnabled
276  onTriggered: {
277  headerContainer.showSearch = true;
278  searchTextField.forceActiveFocus();
279  }
280  },
281  Action {
282  objectName: "settings"
283  text: i18n.ctr("Button: Show the current dash scope settings", "Settings")
284  iconName: "settings"
285  visible: root.settingsEnabled
286  onTriggered: root.settingsClicked()
287  },
288  Action {
289  objectName: "favorite"
290  text: root.favorite ? i18n.tr("Remove from Favorites") : i18n.tr("Add to Favorites")
291  iconName: root.favorite ? "starred" : "non-starred"
292  visible: root.favoriteEnabled
293  onTriggered: root.favoriteClicked()
294  }
295  ]
296  }
297 
298  property var contents: null
299  Component.onCompleted: root.refreshLogo()
300 
301  Component {
302  id: imageComponent
303 
304  Item {
305  anchors { fill: parent; topMargin: units.gu(1.5); bottomMargin: units.gu(1.5) }
306  clip: true
307  Image {
308  objectName: "titleImage"
309  anchors.fill: parent
310  source: root.scopeStyle ? root.scopeStyle.headerLogo : ""
311  fillMode: Image.PreserveAspectFit
312  horizontalAlignment: Image.AlignLeft
313  sourceSize.height: height
314  }
315  }
316  }
317  }
318  }
319  }
320 
321  Component {
322  id: popoverComponent
323  Popover {
324  id: popover
325  autoClose: false
326 
327  property bool unfocusOnDestruction: false
328 
329  Component.onDestruction: {
330  headerContainer.popover = null;
331  if (unfocusOnDestruction) {
332  root.unfocus();
333  }
334  }
335 
336  Column {
337  anchors {
338  top: parent.top
339  left: parent.left
340  right: parent.right
341  }
342 
343  Repeater {
344  id: recentSearches
345  objectName: "recentSearches"
346  model: searchHistory
347 
348  delegate: Standard {
349  showDivider: index < recentSearches.count - 1
350  text: query
351  onClicked: {
352  searchHistory.addQuery(text);
353  searchTextField.text = text;
354  closePopup(/* keepFocus */false);
355  }
356  }
357  }
358  }
359  }
360  }
361 
362  Rectangle {
363  id: bottomBorder
364  visible: showSignatureLine
365  anchors {
366  top: headerContainer.bottom
367  left: parent.left
368  right: parent.right
369  bottom: bottomContainer.top
370  }
371 
372  color: root.scopeStyle ? root.scopeStyle.headerDividerColor : "#e0e0e0"
373 
374  Rectangle {
375  anchors {
376  top: parent.top
377  left: parent.left
378  right: parent.right
379  }
380  height: units.dp(1)
381  color: Qt.darker(parent.color, 1.1)
382  }
383  }
384 
385  Row {
386  visible: bottomBorder.visible
387  spacing: units.gu(.5)
388  Repeater {
389  objectName: "paginationRepeater"
390  model: root.paginationCount
391  Image {
392  objectName: "paginationDots_" + index
393  height: units.gu(1)
394  width: height
395  source: (index == root.paginationIndex) ? "graphics/pagination_dot_on.png" : "graphics/pagination_dot_off.png"
396  }
397  }
398  anchors {
399  top: headerContainer.bottom
400  horizontalCenter: headerContainer.horizontalCenter
401  topMargin: units.gu(.5)
402  }
403  }
404 
405  // FIXME this doesn't work with solid scope backgrounds due to z-ordering
406  Item {
407  id: bottomHighlight
408  visible: bottomBorder.visible
409  anchors {
410  top: bottomContainer.top
411  left: parent.left
412  right: parent.right
413  }
414  z: 1
415  height: units.dp(1)
416  opacity: 0.6
417 
418  // FIXME this should be a shader when bottomItem exists
419  // to support image backgrounds
420  Rectangle {
421  anchors.fill: parent
422  color: if (bottomItem && bottomItem.background) {
423  Qt.lighter(Qt.rgba(bottomItem.background.topColor.r,
424  bottomItem.background.topColor.g,
425  bottomItem.background.topColor.b, 1.0), 1.2);
426  } else if (!bottomItem && root.scopeStyle) {
427  Qt.lighter(Qt.rgba(root.scopeStyle.background.r,
428  root.scopeStyle.background.g,
429  root.scopeStyle.background.b, 1.0), 1.2);
430  } else "#CCFFFFFF"
431  }
432  }
433 
434  Item {
435  id: bottomContainer
436 
437  anchors {
438  left: parent.left
439  right: parent.right
440  bottom: parent.bottom
441  }
442  height: childrenRect.height
443  }
444 }