Unity 8
 All Classes Functions Properties
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.0
18 import Ubuntu.Components 0.1
19 import Ubuntu.Components.Popups 0.1
20 import Ubuntu.Components.ListItems 0.1 as ListItem
21 import Unity 0.2
22 
23 Item {
24  id: root
25  property bool searchEntryEnabled: false
26  property alias searchQuery: searchField.text
27  property ListModel searchHistory
28  property Scope scope
29  property alias childItem: itemContainer.children
30  property alias showBackButton: backButton.visible
31 
32  signal backClicked
33 
34  height: units.gu(8.5)
35  implicitHeight: units.gu(8.5)
36 
37  function triggerSearch() {
38  if (searchEntryEnabled) searchField.forceActiveFocus()
39  }
40 
41  function resetSearch() {
42  if (!searchHistory) return;
43 
44  searchHistory.addQuery(searchField.text);
45  unfocus();
46  searchField.text = "";
47  }
48 
49  function unfocus() {
50  searchField.focus = false;
51  }
52 
53  Connections {
54  target: greeter
55  onShownChanged: if (shown) resetSearch()
56  }
57 
58  Flickable {
59  id: header
60  anchors {
61  left: parent.left
62  right: parent.right
63  top: parent.top
64  }
65  height: units.gu(6.5)
66 
67  interactive: false
68  contentHeight: headerContainer.height
69  clip: true
70 
71  contentY: searchField.activeFocus || searchField.text != "" ? searchContainer.y : headerContainer.y
72 
73  Behavior on contentY { NumberAnimation { duration: 200; easing.type: Easing.OutQuad } }
74 
75  AbstractButton {
76  id: backButton
77  objectName: root.objectName + "_backButton"
78  visible: false
79  height: header.height
80  y: header.contentY
81  anchors {
82  left: parent.left
83  leftMargin: visible ? units.gu(2) : 0
84  }
85  width: visible ? image.width + units.gu(2) : 0
86  onClicked: root.backClicked();
87  Image {
88  id: image
89  anchors.centerIn: parent
90  source: "graphics/headerback.png"
91  }
92  }
93 
94  // FIXME this could potentially be simplified to avoid all the containers
95  Item {
96  id: headerContainer
97 
98  anchors {
99  left: backButton.right
100  right: parent.right
101  }
102  height: childrenRect.height
103 
104  Item {
105  id: itemContainer
106 
107  width: searchContainer.narrowMode ? header.width : header.width - searchContainer.width
108  height: header.height
109  }
110 
111  Item {
112  id: searchContainer
113  objectName: "searchContainer"
114 
115  visible: searchEntryEnabled
116  property bool popoverShouldOpen: false
117  property bool popoverShouldClose: false
118 
119  property bool narrowMode: parent.width < units.gu(80)
120 
121  property bool active: searchField.text != "" || searchField.activeFocus
122  property var popover: null
123 
124  anchors.right: headerContainer.right
125  height: header.height
126 
127  state:
128  if (active && narrowMode) "narrowActive"
129  else if (!active && narrowMode) "narrowInactive"
130  else if (active && !narrowMode) "active"
131  else if (!active && !narrowMode) "inactive"
132 
133  onStateChanged: {
134  if (state == "active" || state == "narrowActive") {
135  popoverShouldOpen = true;
136  popoverShouldClose = false;
137  } else {
138  popoverShouldOpen = false;
139  popoverShouldClose = true;
140  }
141  }
142 
143  function openPopover() {
144  if (searchHistory.count > 0) {
145  searchContainer.popover = PopupUtils.open(popoverComponent, pointerPositioner,
146  {
147  "contentWidth": searchField.width,
148  "edgeMargins": units.gu(1)
149  }
150  )
151  }
152  popoverShouldOpen = false;
153  popoverShouldClose = false;
154  }
155 
156  function closePopover() {
157  if (searchContainer.popover) {
158  PopupUtils.close(searchContainer.popover);
159  searchContainer.popover = null;
160  }
161  popoverShouldOpen = false;
162  popoverShouldClose = false;
163  }
164 
165  onActiveFocusChanged: if (!activeFocus) { searchHistory.addQuery(searchField.text) }
166 
167  TextField {
168  id: searchField
169 
170  anchors.fill: parent
171  anchors.margins: units.gu(1)
172 
173  inputMethodHints: Qt.ImhNoPredictiveText
174  hasClearButton: false
175 
176  primaryItem: AbstractButton {
177  enabled: searchField.text != "" && !searchIndicator.running
178  onClicked: {
179  if (searchField.text != "") {
180  searchHistory.addQuery(searchField.text)
181  searchField.text = ""
182  }
183  }
184  height: parent.height
185  width: height
186 
187  ActivityIndicator {
188  id: searchIndicator
189  objectName: "searchIndicator"
190 
191  anchors {
192  verticalCenter: parent.verticalCenter
193  left: parent.left
194  leftMargin: units.gu(0.5)
195  }
196 
197  running: opacity > 0
198  }
199 
200  Image {
201  id: primaryImage
202  objectName: "primaryImage"
203  anchors {
204  verticalCenter: parent.verticalCenter
205  left: parent.left
206  leftMargin: units.gu(0.5)
207  }
208  width: units.gu(3)
209  height: units.gu(3)
210  visible: opacity > 0
211  }
212 
213  Item {
214  id: pointerPositioner
215  anchors.left: parent.right
216  anchors.leftMargin: units.gu(0.5)
217  anchors.top: parent.bottom
218  }
219  }
220 
221  onTextChanged: {
222  if (text != "") searchContainer.closePopover()
223  else if (text == "" && activeFocus) searchContainer.openPopover()
224  }
225 
226  onActiveFocusChanged: {
227  if (!activeFocus) searchContainer.closePopover()
228  }
229 
230  states: [
231  State {
232  name: "searching"
233  when: scope && scope.searchInProgress
234  PropertyChanges { target: searchIndicator; running: true; opacity: 1 }
235  PropertyChanges { target: primaryImage; opacity: 0 }
236  },
237  State {
238  name: "idle"
239  when: !scope || !scope.searchInProgress
240  PropertyChanges { target: searchIndicator; opacity: 0 }
241  PropertyChanges { target: primaryImage; opacity: 1 }
242  }
243  ]
244 
245  transitions: [
246  Transition {
247  to: "searching"
248  reversible: true
249  SequentialAnimation {
250  NumberAnimation { target: primaryImage; property: "opacity"; duration: UbuntuAnimation.FastDuration; easing.type: Easing.Linear }
251  NumberAnimation { target: searchIndicator; property: "opacity"; duration: UbuntuAnimation.FastDuration; easing.type: Easing.Linear }
252  }
253  }
254  ]
255  }
256 
257  states: [
258  State {
259  name: "wide"
260  AnchorChanges { target: itemContainer; anchors.top: headerContainer.top }
261  AnchorChanges { target: searchContainer; anchors.left: undefined; anchors.top: itemContainer.top }
262  },
263  State {
264  name: "narrow"
265  PropertyChanges { target: searchField; highlighted: true }
266  AnchorChanges { target: itemContainer; anchors.top: searchContainer.bottom }
267  AnchorChanges { target: searchContainer; anchors.left: headerContainer.left; anchors.top: headerContainer.top }
268  },
269  State {
270  name: "active"
271  extend: "wide"
272  PropertyChanges { target: searchContainer; width: units.gu(40) }
273  PropertyChanges { target: primaryImage; source: searchField.text ? "../Dash/graphics/icon_clear.png" : "../Dash/graphics/icon_search_active.png" }
274  PropertyChanges { target: searchField; highlighted: true }
275  },
276  State {
277  name: "inactive"
278  extend: "wide"
279  PropertyChanges { target: searchContainer; width: units.gu(25) }
280  PropertyChanges { target: primaryImage; source: "../Dash/graphics/icon_search_inactive.png" }
281  PropertyChanges { target: searchField; highlighted: false }
282  },
283  State {
284  name: "narrowActive"
285  extend: "narrow"
286  PropertyChanges { target: header; contentY: 0 }
287  PropertyChanges { target: primaryImage; source: searchField.text ? "../Dash/graphics/icon_clear.png" : "../Dash/graphics/icon_search_active.png" }
288  },
289  State {
290  name: "narrowInactive"
291  extend: "narrow"
292  PropertyChanges { target: header; contentY: header.height }
293  PropertyChanges { target: primaryImage; source: searchField.text ? "../Dash/graphics/icon_clear.png" : "../Dash/graphics/icon_search_active.png" }
294  }
295  ]
296 
297  transitions: [
298  Transition {
299  to: "active"
300  SequentialAnimation {
301  ParallelAnimation {
302  NumberAnimation { targets: [searchContainer, searchField]; property: "width"; duration: 200; easing.type: Easing.InOutQuad }
303  PropertyAction { target: primaryImage; property: "source" }
304  AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
305  }
306  ScriptAction { script: if (searchContainer.popoverShouldOpen) { searchContainer.openPopover(); } }
307  }
308  },
309  Transition {
310  to: "inactive"
311  ScriptAction { script: if (searchContainer.popoverShouldClose) { searchContainer.closePopover(); } }
312  NumberAnimation { targets: [searchContainer, searchField] ; property: "width"; duration: 200; easing.type: Easing.InOutQuad }
313  AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
314  },
315  Transition {
316  to: "narrowActive"
317  SequentialAnimation {
318  ParallelAnimation {
319  NumberAnimation { targets: [searchContainer, searchField] ; property: "width"; duration: 200; easing.type: Easing.OutQuad }
320  AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
321  }
322  ScriptAction { script: if (searchContainer.popoverShouldOpen) { searchContainer.openPopover(); } }
323  }
324  },
325  Transition {
326  to: "narrowInactive"
327  ScriptAction { script: if (searchContainer.popoverShouldClose) { searchContainer.closePopover(); } }
328  NumberAnimation { targets: [searchContainer, searchField] ; property: "width"; duration: 200; easing.type: Easing.OutQuad }
329  AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
330  }
331  ]
332 
333  Component {
334  id: popoverComponent
335  Popover {
336  id: popover
337 
338  // FIXME: this should go into the first item below, but enable: false
339  // prevents mouse events propagation
340  AbstractButton {
341  anchors {
342  top: parent.top
343  right: parent.right
344  }
345  height: units.gu(6)
346  width: height
347 
348  onClicked: searchContainer.closePopover()
349 
350  Image {
351  anchors.centerIn: parent
352  width: units.gu(2)
353  height: units.gu(2)
354  source: "../Dash/graphics/icon_listview_clear.png"
355  }
356  }
357 
358  Column {
359  anchors {
360  top: parent.top
361  left: parent.left
362  right: parent.right
363  }
364 
365  ListItem.Standard { enabled: false; text: i18n.tr("Recent searches") }
366 
367  Repeater {
368  id: recentSearches
369  model: searchHistory
370 
371  delegate: ListItem.Standard {
372  showDivider: index < recentSearches.count - 1
373  text: query
374  onClicked: {
375  searchHistory.addQuery(text)
376  searchField.text = text
377  }
378  }
379  }
380  }
381  }
382  }
383 
384  InverseMouseArea {
385  enabled: searchField.activeFocus
386 
387  anchors {
388  top: parent.top
389  left: parent.left
390  right: parent.right
391  }
392 
393  height: searchContainer.popover ? parent.height + searchContainer.popover.contentHeight + units.gu(2) : parent.height
394 
395  onPressed: searchField.focus = false
396  }
397  }
398  }
399  }
400 
401  BorderImage {
402  id: bottomBorder
403  anchors {
404  top: header.bottom
405  left: parent.left
406  right: parent.right
407  bottom: parent.bottom
408  }
409 
410  source: "graphics/PageHeaderBaseDivider.sci"
411  }
412 }