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