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