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