on 44319ca help sign in
unity8/Components/PageHeader.qml
396 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 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: {
95 if (root.childItem.length == 0)
96 return false;
97 if (root.childItem.length > 1) {
98 console.log("ERROR: PageHeader childItem must be a single item");
99 return false;
100 }
101 var item = root.childItem[0];
102 return parent.width < item.implicitWidth + units.gu(50)
103 }
105 property bool active: searchField.text != "" || searchField.activeFocus
106 property var popover: null
108 anchors.right: headerContainer.right
109 height: header.height
111 state:
112 if (active && narrowMode) "narrowActive"
113 else if (!active && narrowMode) "narrowInactive"
114 else if (active && !narrowMode) "active"
115 else if (!active && !narrowMode) "inactive"
117 onStateChanged: {
118 if (state == "active" || state == "narrowActive") {
119 popoverShouldOpen = true;
120 popoverShouldClose = false;
121 } else {
122 popoverShouldOpen = false;
123 popoverShouldClose = true;
124 }
125 }
127 function openPopover() {
128 if (searchHistory.count > 0) {
129 searchContainer.popover = PopupUtils.open(popoverComponent, searchField,
130 {
131 "pointerTarget": pointerPositioner,
132 "contentWidth": searchField.width,
133 "edgeMargins": units.gu(1)
134 }
135 )
136 }
137 popoverShouldOpen = false;
138 popoverShouldClose = false;
139 }
141 function closePopover() {
142 if (searchContainer.popover) {
143 PopupUtils.close(searchContainer.popover);
144 searchContainer.popover = null;
145 }
146 popoverShouldOpen = false;
147 popoverShouldClose = false;
148 }
150 onActiveFocusChanged: if (!activeFocus) { searchHistory.addQuery(searchField.text) }
152 TextField {
153 id: searchField
155 anchors.fill: parent
156 anchors.margins: units.gu(1)
158 hasClearButton: false
160 primaryItem: AbstractButton {
161 enabled: searchField.text != "" && !searchIndicator.running
162 onClicked: {
163 if (searchField.text != "") {
164 searchHistory.addQuery(searchField.text)
165 searchField.text = ""
166 }
167 }
168 height: parent.height
169 width: height
171 ActivityIndicator {
172 id: searchIndicator
173 objectName: "searchIndicator"
175 anchors {
176 verticalCenter: parent.verticalCenter
177 left: parent.left
178 leftMargin: units.gu(0.5)
179 }
181 running: opacity > 0
182 }
184 Image {
185 id: primaryImage
186 objectName: "primaryImage"
187 anchors {
188 verticalCenter: parent.verticalCenter
189 left: parent.left
190 leftMargin: units.gu(0.5)
191 }
192 width: units.gu(3)
193 height: units.gu(3)
194 visible: opacity > 0
195 }
197 Item {
198 id: pointerPositioner
199 anchors.left: parent.right
200 anchors.leftMargin: units.gu(0.5)
201 anchors.top: parent.bottom
202 }
203 }
205 onTextChanged: {
206 if (text != "") searchContainer.closePopover()
207 else if (text == "" && activeFocus) searchContainer.openPopover()
208 }
210 onActiveFocusChanged: {
211 if (!activeFocus) searchContainer.closePopover()
212 }
214 states: [
215 State {
216 name: "searching"
217 when: scope && scope.searchInProgress
218 PropertyChanges { target: searchIndicator; running: true; opacity: 1 }
219 PropertyChanges { target: primaryImage; opacity: 0 }
220 },
221 State {
222 name: "idle"
223 when: !scope || !scope.searchInProgress
224 PropertyChanges { target: searchIndicator; opacity: 0 }
225 PropertyChanges { target: primaryImage; opacity: 1 }
226 }
227 ]
229 transitions: [
230 Transition {
231 to: "searching"
232 reversible: true
233 SequentialAnimation {
234 NumberAnimation { target: primaryImage; property: "opacity"; duration: UbuntuAnimation.FastDuration; easing.type: Easing.Linear }
235 NumberAnimation { target: searchIndicator; property: "opacity"; duration: UbuntuAnimation.FastDuration; easing.type: Easing.Linear }
236 }
237 }
238 ]
239 }
241 states: [
242 State {
243 name: "wide"
244 AnchorChanges { target: itemContainer; anchors.top: headerContainer.top }
245 AnchorChanges { target: searchContainer; anchors.left: undefined; anchors.top: itemContainer.top }
246 },
247 State {
248 name: "narrow"
249 PropertyChanges { target: searchField; highlighted: true }
250 AnchorChanges { target: itemContainer; anchors.top: searchContainer.bottom }
251 AnchorChanges { target: searchContainer; anchors.left: headerContainer.left; anchors.top: headerContainer.top }
252 },
253 State {
254 name: "active"
255 extend: "wide"
256 PropertyChanges { target: searchContainer; width: units.gu(40) }
257 PropertyChanges { target: primaryImage; source: searchField.text ? "../Dash/graphics/icon_clear.png" : "../Dash/graphics/icon_search_active.png" }
258 PropertyChanges { target: searchField; highlighted: true }
259 },
260 State {
261 name: "inactive"
262 extend: "wide"
263 PropertyChanges { target: searchContainer; width: units.gu(25) }
264 PropertyChanges { target: primaryImage; source: "../Dash/graphics/icon_search_inactive.png" }
265 PropertyChanges { target: searchField; highlighted: false }
266 },
267 State {
268 name: "narrowActive"
269 extend: "narrow"
270 PropertyChanges { target: header; contentY: 0 }
271 PropertyChanges { target: primaryImage; source: searchField.text ? "../Dash/graphics/icon_clear.png" : "../Dash/graphics/icon_search_active.png" }
272 },
273 State {
274 name: "narrowInactive"
275 extend: "narrow"
276 PropertyChanges { target: header; contentY: header.height }
277 PropertyChanges { target: primaryImage; source: searchField.text ? "../Dash/graphics/icon_clear.png" : "../Dash/graphics/icon_search_active.png" }
278 }
279 ]
281 transitions: [
282 Transition {
283 to: "active"
284 SequentialAnimation {
285 ParallelAnimation {
286 NumberAnimation { targets: [searchContainer, searchField]; property: "width"; duration: 200; easing.type: Easing.InOutQuad }
287 PropertyAction { target: primaryImage; property: "source" }
288 AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
289 }
290 ScriptAction { script: if (searchContainer.popoverShouldOpen) { searchContainer.openPopover(); } }
291 }
292 },
293 Transition {
294 to: "inactive"
295 ScriptAction { script: if (searchContainer.popoverShouldClose) { searchContainer.closePopover(); } }
296 NumberAnimation { targets: [searchContainer, searchField] ; property: "width"; duration: 200; easing.type: Easing.InOutQuad }
297 AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
298 },
299 Transition {
300 to: "narrowActive"
301 SequentialAnimation {
302 ParallelAnimation {
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 ScriptAction { script: if (searchContainer.popoverShouldOpen) { searchContainer.openPopover(); } }
307 }
308 },
309 Transition {
310 to: "narrowInactive"
311 ScriptAction { script: if (searchContainer.popoverShouldClose) { searchContainer.closePopover(); } }
312 NumberAnimation { targets: [searchContainer, searchField] ; property: "width"; duration: 200; easing.type: Easing.OutQuad }
313 AnchorAnimation { targets: [searchContainer, itemContainer]; duration: 200; easing.type: Easing.InOutQuad }
314 }
315 ]
317 Component {
318 id: popoverComponent
319 Popover {
320 id: popover
322 // FIXME: this should go into the first item below, but enable: false
323 // prevents mouse events propagation
324 AbstractButton {
325 anchors {
326 top: parent.top
327 right: parent.right
328 }
329 height: units.gu(6)
330 width: height
332 onClicked: searchContainer.closePopover()
334 Image {
335 anchors.centerIn: parent
336 width: units.gu(2)
337 height: units.gu(2)
338 source: "../Dash/graphics/icon_listview_clear.png"
339 }
340 }
342 Column {
343 anchors {
344 top: parent.top
345 left: parent.left
346 right: parent.right
347 }
349 ListItem.Standard { enabled: false; text: i18n.tr("Recent searches") }
351 Repeater {
352 id: recentSearches
353 model: searchHistory
355 delegate: ListItem.Standard {
356 showDivider: index < recentSearches.count - 1
357 text: query
358 onClicked: {
359 searchHistory.addQuery(text)
360 searchField.text = text
361 }
362 }
363 }
364 }
365 }
366 }
368 InverseMouseArea {
369 enabled: searchField.activeFocus
371 anchors {
372 top: parent.top
373 left: parent.left
374 right: parent.right
375 }
377 height: searchContainer.popover ? parent.height + searchContainer.popover.contentHeight + units.gu(2) : parent.height
379 onPressed: searchField.focus = false
380 }
381 }
382 }
383 }
385 BorderImage {
386 id: bottomBorder
387 anchors {
388 top: header.bottom
389 left: parent.left
390 right: parent.right
391 bottom: parent.bottom
392 }
394 source: "graphics/PageHeaderBaseDivider.sci"
395 }