Merge lp:~fboucault/ubuntu-ui-toolkit/text_input_larger_selection_handle_on_trunk into lp:~bzoltan/ubuntu-ui-toolkit/trunk
- text_input_larger_selection_handle_on_trunk
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Zoltan Balogh |
Approved revision: | 1142 |
Merged at revision: | 1123 |
Proposed branch: | lp:~fboucault/ubuntu-ui-toolkit/text_input_larger_selection_handle_on_trunk |
Merge into: | lp:~bzoltan/ubuntu-ui-toolkit/trunk |
Diff against target: |
779 lines (+310/-183) 12 files modified
modules/Ubuntu/Components/InputHandler.qml (+4/-2) modules/Ubuntu/Components/Popups/PopupBase.qml (+3/-1) modules/Ubuntu/Components/TextArea.qml (+2/-0) modules/Ubuntu/Components/TextCursor.qml (+135/-99) modules/Ubuntu/Components/TextField.qml (+30/-28) modules/Ubuntu/Components/TextInputPopover.qml (+20/-5) modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml (+2/-1) modules/Ubuntu/Components/plugin/ucmousefilters.cpp (+1/-0) tests/autopilot/ubuntuuitoolkit/base.py (+16/-0) tests/autopilot/ubuntuuitoolkit/tests/components/test_textinput.py (+95/-12) tests/unit_x11/tst_components/tst_textinput_common.qml (+1/-33) tests/unit_x11/tst_components/tst_textinput_touch.qml (+1/-2) |
To merge this branch: | bzr merge lp:~fboucault/ubuntu-ui-toolkit/text_input_larger_selection_handle_on_trunk |
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Cris Dywan (community) | Needs Fixing | ||
Tim Peeters | Pending | ||
Zsombor Egri | Pending | ||
Zoltan Balogh | Pending | ||
Review via email: mp+241024@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-11-11.
Commit message
Many TextField/TextArea fixes improving user experience greatly and making it more in line with the design:
- handle much easier to drag by making its touch area 4 grid units wide and centered around the cursor
- double tapping anywhere (even around the handle) selects the word and opens the contextual menu
- long pressing anywhere (even around the handle) selects the word and opens the contextual menu
- make sure that when there is a selection the contextual menu is shown and shown above the cursor at the beginning of the selection
- do not prevent other UI elements from receiving mouse/touch events when the contextual menu is shown
Description of the change
Florian Boucault (fboucault) wrote : | # |
- 1124. By Florian Boucault
-
Dismiss popover on copy.
- 1125. By Florian Boucault
-
Disable select all if there is nothing to select
- 1126. By Florian Boucault
-
Reverted handle changes
- 1127. By Florian Boucault
-
Do not disable 'cut' operation when there is nothing to paste but instead when the field is read only.
- 1128. By Florian Boucault
-
Reveted changes to push_to_phone.sh
- 1129. By Florian Boucault
-
Added bug fixes tags.
- 1130. By Florian Boucault
-
Updated components.api
- 1131. By Florian Boucault
-
Sync enabled of Ubuntu.Mouse and MouseArea.
- 1132. By Florian Boucault
-
Make sure popup disappears upon drag and reappears when finished.
Make sure that when not blinking the cursor is visible.
Make sure that tapping somewhere else in the field when in insert mode closes the contextual menu. - 1133. By Florian Boucault
-
Removed unused code.
- 1134. By Florian Boucault
-
Removed incorrect test.
- 1135. By Florian Boucault
-
More robust test_clear_
selection_ on_click - 1136. By Florian Boucault
-
Fixed components:
:TextInputTouch Tests:: test_long_ tap_on_ selected_ text - 1137. By Florian Boucault
-
Fixed test_select_
text_by_ dragging_ cursor_ handler - 1138. By Florian Boucault
-
Fixed test_0_
drag_autosizing _textarea_ drags_parent_ flickable
Cris Dywan (kalikiana) wrote : | # |
> handle much easier to drag by making its touch area
> 4 grid units wide and centered around the cursor
I'm finding it very hard to drag the handles at all, with or without selection. In fact harder than before.
Cris Dywan (kalikiana) wrote : | # |
This needs test cases. As this effectively overlaps with lp:~ubuntu-sdk-team/ubuntu-ui-toolkit/insertMode taking test cases from there is probably a good idea, and we'd pick up what's left from there after having this one done.
Cris Dywan (kalikiana) wrote : | # |
Pondering: it might be that the knob, as opposed to the |, is what's doing poorly for me. If I try to aim for the | always it's fairly fine.
- 1139. By Florian Boucault
-
TextInput: move clear button on top so that it takes priority over tapping on the cursor.
Florian Boucault (fboucault) wrote : | # |
> Pondering: it might be that the knob, as opposed to the |, is what's doing
> poorly for me. If I try to aim for the | always it's fairly fine.
That's the design indeed, users are expected to target the cursor.
- 1140. By Florian Boucault
-
Added autopilot tests for select and insert modes.
- 1141. By Florian Boucault
-
TextInput: move clear button on top so that it takes priority over tapping on the cursor.
- 1142. By Florian Boucault
-
Fixed pep8 failure
Preview Diff
1 | === modified file 'modules/Ubuntu/Components/InputHandler.qml' |
2 | --- modules/Ubuntu/Components/InputHandler.qml 2014-09-20 08:36:48 +0000 |
3 | +++ modules/Ubuntu/Components/InputHandler.qml 2014-11-10 18:31:06 +0000 |
4 | @@ -392,7 +392,7 @@ |
5 | } |
6 | function handleDblClick(event, touch) { |
7 | if (main.selectByMouse) { |
8 | - input.selectWord(); |
9 | + openContextMenu(event, false); |
10 | // turn selection state temporarily so the selection is not cleared on release |
11 | state = "selection"; |
12 | if (touch) { |
13 | @@ -448,13 +448,15 @@ |
14 | } |
15 | |
16 | // do not open context menu if this is scrolling |
17 | - if (touchPoint.startY - touchPoint.y < -units.dp(2)) |
18 | + if (touchPoint.startY - touchPoint.y < -units.gu(2)) |
19 | return; |
20 | |
21 | openContextMenu(touchPoint, false); |
22 | suppressReleaseEvent = true; |
23 | } |
24 | } |
25 | + |
26 | + property bool doubleTapInProgress: doubleTap.running |
27 | Timer { |
28 | id: doubleTap |
29 | property int tapCount: 0 |
30 | |
31 | === modified file 'modules/Ubuntu/Components/Popups/PopupBase.qml' |
32 | --- modules/Ubuntu/Components/Popups/PopupBase.qml 2014-08-22 15:02:28 +0000 |
33 | +++ modules/Ubuntu/Components/Popups/PopupBase.qml 2014-11-10 18:31:06 +0000 |
34 | @@ -240,7 +240,9 @@ |
35 | ScriptAction { |
36 | script: { |
37 | popupBase.visible = false; |
38 | - stateWrapper.restoreActiveFocus(); |
39 | + if (eventGrabber.enabled) { |
40 | + stateWrapper.restoreActiveFocus(); |
41 | + } |
42 | } |
43 | } |
44 | } |
45 | |
46 | === modified file 'modules/Ubuntu/Components/TextArea.qml' |
47 | --- modules/Ubuntu/Components/TextArea.qml 2014-10-01 18:04:10 +0000 |
48 | +++ modules/Ubuntu/Components/TextArea.qml 2014-11-10 18:31:06 +0000 |
49 | @@ -16,6 +16,7 @@ |
50 | |
51 | import QtQuick 2.0 |
52 | import Ubuntu.Components 1.1 as Ubuntu |
53 | +import Ubuntu.Components.Popups 1.0 |
54 | import "mathUtils.js" as MathUtils |
55 | |
56 | /*! |
57 | @@ -847,6 +848,7 @@ |
58 | mouseSelectionMode: TextEdit.SelectWords |
59 | selectByMouse: true |
60 | activeFocusOnPress: true |
61 | + onActiveFocusChanged: if (!activeFocus && inputHandler.popover) PopupUtils.close(inputHandler.popover) |
62 | cursorDelegate: TextCursor { |
63 | handler: inputHandler |
64 | } |
65 | |
66 | === modified file 'modules/Ubuntu/Components/TextCursor.qml' |
67 | --- modules/Ubuntu/Components/TextCursor.qml 2014-10-07 15:21:11 +0000 |
68 | +++ modules/Ubuntu/Components/TextCursor.qml 2014-11-10 18:31:06 +0000 |
69 | @@ -62,33 +62,58 @@ |
70 | The function opens the text input popover setting the text cursor as caller. |
71 | */ |
72 | Connections { |
73 | - target: inputHandler |
74 | - onPressAndHold: openPopover() |
75 | + target: handler |
76 | + onPressAndHold: { |
77 | + // open context menu only for cursorPosition or selectionStart as to |
78 | + // ensure that only one popover gets opened |
79 | + if (positionProperty !== "selectionEnd") { |
80 | + openPopover(); |
81 | + } |
82 | + } |
83 | onTextModified: typing = true |
84 | - onTap: typing = false |
85 | + onTap: { |
86 | + typing = false |
87 | + if (handler.popover != null) { |
88 | + PopupUtils.close(handler.popover); |
89 | + } |
90 | + } |
91 | } |
92 | |
93 | function openPopover() { |
94 | - if (!visible || opacity === 0.0 || dragger.drag.active) { |
95 | - return; |
96 | - } |
97 | - // open context menu only for cursorPosition or selectionEnd |
98 | - if (positionProperty !== "selectionStart") { |
99 | - if (inputHandler.popover != null) |
100 | - inputHandler.popover.hide(); |
101 | - |
102 | - var component = handler.main.popover; |
103 | - if (component === undefined) |
104 | - component = Qt.resolvedUrl("TextInputPopover.qml"); |
105 | - var popup = PopupUtils.open(component, cursorItem, { |
106 | - "target": handler.main |
107 | - }); |
108 | - contextMenuVisible = true; |
109 | - popup.onVisibleChanged.connect(contextMenuHidden.bind(undefined, popup)); |
110 | - // do not grab focus! |
111 | - popup.__foreground.activeFocusOnPress = false; |
112 | - inputHandler.popover = popup; |
113 | - } |
114 | + if (!visible || opacity === 0.0 || dragger.dragActive) { |
115 | + return; |
116 | + } |
117 | + |
118 | + if (contextMenuVisible) { |
119 | + return; |
120 | + } |
121 | + |
122 | + if (handler.popover != null) { |
123 | + PopupUtils.close(handler.popover); |
124 | + } |
125 | + |
126 | + var component = handler.main.popover; |
127 | + if (component === undefined) |
128 | + component = Qt.resolvedUrl("TextInputPopover.qml"); |
129 | + |
130 | + var popup; |
131 | + if (fakeCursor.visible) { |
132 | + popup = PopupUtils.open(component, cursorItem, { |
133 | + "target": handler.main, |
134 | + }); |
135 | + } else { |
136 | + // if the cursor is out of the visible viewport, anchor the |
137 | + // contextual menu to the input field |
138 | + popup = PopupUtils.open(component, handler.main, { |
139 | + "target": handler.main, |
140 | + }); |
141 | + cursorItem.Component.onDestruction.connect(popup.__closePopup); |
142 | + } |
143 | + contextMenuVisible = true; |
144 | + popup.onVisibleChanged.connect(contextMenuHidden.bind(undefined, popup)); |
145 | + // do not grab focus! |
146 | + popup.__foreground.activeFocusOnPress = false; |
147 | + handler.popover = popup; |
148 | } |
149 | |
150 | visible: handler.main.cursorVisible |
151 | @@ -133,82 +158,62 @@ |
152 | function contextMenuHidden(p) { |
153 | contextMenuVisible = false |
154 | } |
155 | - onXChanged: if (draggedItem.state === "") draggedItem.moveToCaret() |
156 | - onYChanged: if (draggedItem.state === "") draggedItem.moveToCaret() |
157 | + |
158 | + onXChanged: if (!draggedItemMouseArea.pressed) draggedItem.moveToCaret() |
159 | + onYChanged: if (!draggedItemMouseArea.pressed) draggedItem.moveToCaret() |
160 | Component.onCompleted: draggedItem.moveToCaret() |
161 | |
162 | //dragged item |
163 | Item { |
164 | id: draggedItem |
165 | objectName: cursorItem.positionProperty + "_draggeditem" |
166 | - width: caret ? Math.max(caret.width, units.gu(2)) : 0 |
167 | - height: caret ? Math.max(caret.height, units.gu(2)) : 0 |
168 | + width: caret ? units.gu(4) : 0 |
169 | + height: caret ? cursorItem.height : 0 |
170 | parent: handler.main |
171 | visible: cursorItem.visible && (cursorItem.opacity > 0.0) && QuickUtils.touchScreenAvailable |
172 | |
173 | - // when the dragging ends, reposition the dragger back to caret |
174 | - onStateChanged: { |
175 | - if (state === "") { |
176 | - draggedItem.moveToCaret(); |
177 | - } |
178 | - } |
179 | - |
180 | /* |
181 | Mouse area to turn on dragging or selection mode when pressed |
182 | on the handler area. The drag mode is turned off when the drag |
183 | gets inactive or when the LeftButton is released. |
184 | */ |
185 | MouseArea { |
186 | + id: draggedItemMouseArea |
187 | objectName: cursorItem.positionProperty + "_activator" |
188 | anchors.fill: parent |
189 | acceptedButtons: Qt.LeftButton |
190 | preventStealing: true |
191 | - enabled: parent.width && parent.height && parent.visible |
192 | - |
193 | - onPressed: { |
194 | - draggedItem.moveToCaret(mouse.x, mouse.y); |
195 | - draggedItem.state = "dragging"; |
196 | + enabled: parent.width && parent.height && parent.visible && !handler.doubleTapInProgress |
197 | + onPressedChanged: { |
198 | + if (!pressed) { |
199 | + // when the dragging ends, reposition the dragger back to caret |
200 | + draggedItem.moveToCaret(); |
201 | + } |
202 | } |
203 | Ubuntu.Mouse.forwardTo: [dragger] |
204 | - /* |
205 | - As we are forwarding the events to the upper mouse area, the release |
206 | - will not get into the normal MosueArea onRelease signal as the preventStealing |
207 | - will not have any effect on the handling. However due to the mouse |
208 | - filter's nature, we will still be able to grab mouse events and we |
209 | - can stop dragging. We only handle the release in case the drag hasn't |
210 | - been active at all, otherwise the drag will not be deactivated and we |
211 | - will end up in a binding loop on the moveToCaret() next time the caret |
212 | - handler is grabbed. |
213 | - */ |
214 | - Ubuntu.Mouse.onReleased: { |
215 | - if (!dragger.drag.active) { |
216 | - draggedItem.state = ""; |
217 | - } |
218 | + Ubuntu.Mouse.onClicked: openPopover() |
219 | + Ubuntu.Mouse.onPressAndHold: { |
220 | + handler.main.selectWord(); |
221 | + handler.pressAndHold(-1); |
222 | } |
223 | + Ubuntu.Mouse.onDoubleClicked: handler.main.selectWord() |
224 | + Ubuntu.Mouse.clickAndHoldThreshold: units.gu(2) |
225 | + Ubuntu.Mouse.enabled: enabled |
226 | } |
227 | |
228 | // aligns the draggedItem to the caret and resets the dragger |
229 | - function moveToCaret(cx, cy) { |
230 | - if (cx === undefined && cy === undefined) { |
231 | - cx = mappedCursorPosition("x") + caretX; |
232 | - cy = mappedCursorPosition("y") + caretY; |
233 | - } else { |
234 | - // move mouse position to caret |
235 | - cx += draggedItem.x; |
236 | - cy += draggedItem.y; |
237 | - } |
238 | - |
239 | - draggedItem.x = cx; |
240 | - draggedItem.y = cy; |
241 | - dragger.resetDrag(); |
242 | + function moveToCaret() { |
243 | + draggedItem.x = mappedCursorPosition("x") - draggedItem.width / 2; |
244 | + draggedItem.y = mappedCursorPosition("y"); |
245 | } |
246 | - // positions caret to the dragged position |
247 | + // positions caret to the dragged posinotion |
248 | function positionCaret() { |
249 | - if (dragger.drag.active) { |
250 | - var dx = dragger.thumbStartX + dragger.dragAmountX + handler.flickable.contentX; |
251 | - var dy = dragger.thumbStartY + dragger.dragAmountY + handler.flickable.contentY; |
252 | + if (dragger.dragActive) { |
253 | + var dx = dragger.dragStartX + dragger.dragAmountX + handler.flickable.contentX; |
254 | + var dy = dragger.dragStartY + dragger.dragAmountY + handler.flickable.contentY; |
255 | // consider only the x-distance because of the overlays |
256 | dx -= handler.frameDistance.x; |
257 | + dy -= handler.frameDistance.y; |
258 | handler.positionCaret(positionProperty, dx, dy); |
259 | } |
260 | } |
261 | @@ -219,38 +224,69 @@ |
262 | // fill the entire component area |
263 | parent: handler.main |
264 | anchors.fill: parent |
265 | - hoverEnabled: true |
266 | - preventStealing: drag.active |
267 | - enabled: draggedItem.enabled && draggedItem.state === "dragging" && QuickUtils.touchScreenAvailable |
268 | + enabled: draggedItemMouseArea.enabled && draggedItemMouseArea.pressed && QuickUtils.touchScreenAvailable |
269 | + onEnabledChanged: { |
270 | + if (enabled) { |
271 | + dragAmountX = 0; |
272 | + dragAmountY = 0; |
273 | + firstMouseXChange = true; |
274 | + firstMouseYChange = true; |
275 | + } else { |
276 | + dragActive = false; |
277 | + } |
278 | + } |
279 | |
280 | - property int thumbStartX |
281 | property int dragStartX |
282 | - property int dragAmountX: dragger.drag.target.x - dragStartX |
283 | - property int thumbStartY |
284 | + property int dragAmountX |
285 | property int dragStartY |
286 | - property int dragAmountY: dragger.drag.target.y - dragStartY |
287 | - |
288 | - function resetDrag() { |
289 | - thumbStartX = mappedCursorPosition("x"); |
290 | - thumbStartY = mappedCursorPosition("y"); |
291 | - dragStartX = drag.target ? drag.target.x : 0; |
292 | - dragStartY = drag.target ? drag.target.y : 0; |
293 | - } |
294 | - |
295 | - // do not set minimum/maximum so we can drag outside of the Flickable area |
296 | - drag { |
297 | - target: draggedItem |
298 | - axis: handler.singleLine ? Drag.XAxis : Drag.XAndYAxis |
299 | - // deactivate dragging |
300 | - onActiveChanged: { |
301 | - if (!drag.active) { |
302 | - draggedItem.state = ""; |
303 | - } |
304 | - } |
305 | - } |
306 | - |
307 | - onDragAmountXChanged: draggedItem.positionCaret() |
308 | - onDragAmountYChanged: draggedItem.positionCaret() |
309 | + property int dragAmountY |
310 | + property bool dragActive: false |
311 | + property int dragThreshold: units.gu(2) |
312 | + property bool firstMouseXChange: true |
313 | + property bool firstMouseYChange: true |
314 | + |
315 | + onMouseXChanged: { |
316 | + if (firstMouseXChange) { |
317 | + dragStartX = mouseX; |
318 | + firstMouseXChange = false; |
319 | + } else { |
320 | + var amount = mouseX - dragStartX; |
321 | + if (Math.abs(amount) >= dragThreshold) { |
322 | + dragActive = true; |
323 | + } |
324 | + if (dragActive) { |
325 | + dragAmountX = amount; |
326 | + draggedItem.positionCaret(); |
327 | + } |
328 | + } |
329 | + } |
330 | + |
331 | + onMouseYChanged: { |
332 | + if (firstMouseYChange) { |
333 | + dragStartY = mouseY; |
334 | + firstMouseYChange = false; |
335 | + } else { |
336 | + var amount = mouseY - dragStartY; |
337 | + if (Math.abs(amount) >= dragThreshold) { |
338 | + dragActive = true; |
339 | + } |
340 | + if (dragActive) { |
341 | + dragAmountY = amount; |
342 | + draggedItem.positionCaret() |
343 | + } |
344 | + } |
345 | + } |
346 | + |
347 | + onDragActiveChanged: { |
348 | + // close contextual menu when dragging and reopen it at the end of the drag |
349 | + if (dragActive) { |
350 | + if (handler.popover != null) { |
351 | + PopupUtils.close(handler.popover); |
352 | + } |
353 | + } else { |
354 | + handler.pressAndHold(-1); |
355 | + } |
356 | + } |
357 | } |
358 | |
359 | // fake cursor, caret is reparented to it to avoid caret clipping |
360 | |
361 | === modified file 'modules/Ubuntu/Components/TextField.qml' |
362 | --- modules/Ubuntu/Components/TextField.qml 2014-10-02 20:08:52 +0000 |
363 | +++ modules/Ubuntu/Components/TextField.qml 2014-11-10 18:31:06 +0000 |
364 | @@ -16,6 +16,7 @@ |
365 | |
366 | import QtQuick 2.0 |
367 | import Ubuntu.Components 1.1 as Ubuntu |
368 | +import Ubuntu.Components.Popups 1.0 |
369 | |
370 | /*! |
371 | \qmltype TextField |
372 | @@ -901,34 +902,6 @@ |
373 | } |
374 | } |
375 | |
376 | - AbstractButton { |
377 | - id: clearButton |
378 | - objectName: "clear_button" |
379 | - activeFocusOnPress: false |
380 | - |
381 | - anchors { |
382 | - top: parent.top |
383 | - right: rightPane.left |
384 | - bottom: parent.bottom |
385 | - margins: internal.spacing |
386 | - } |
387 | - width: visible ? icon.width : 0 |
388 | - visible: control.hasClearButton && |
389 | - !control.readOnly && |
390 | - (control.activeFocus && ((editor.text != "") || editor.inputMethodComposing)) |
391 | - |
392 | - Icon { |
393 | - id: icon |
394 | - anchors.verticalCenter: parent.verticalCenter |
395 | - width: units.gu(2.5) |
396 | - height: width |
397 | - // use icon from icon-theme |
398 | - name: control.hasClearButton && !control.readOnly ? "clear-search" : "" |
399 | - } |
400 | - |
401 | - onClicked: editor.text = "" |
402 | - } |
403 | - |
404 | // hint text |
405 | Label { |
406 | id: hint |
407 | @@ -991,6 +964,7 @@ |
408 | // overrides |
409 | selectByMouse: true |
410 | activeFocusOnPress: true |
411 | + onActiveFocusChanged: if (!activeFocus && inputHandler.popover) PopupUtils.close(inputHandler.popover) |
412 | |
413 | // input selection and navigation handling |
414 | Ubuntu.Mouse.forwardTo: [inputHandler] |
415 | @@ -1010,6 +984,34 @@ |
416 | } |
417 | } |
418 | |
419 | + AbstractButton { |
420 | + id: clearButton |
421 | + objectName: "clear_button" |
422 | + activeFocusOnPress: false |
423 | + |
424 | + anchors { |
425 | + top: parent.top |
426 | + right: rightPane.left |
427 | + bottom: parent.bottom |
428 | + margins: internal.spacing |
429 | + } |
430 | + width: visible ? icon.width : 0 |
431 | + visible: control.hasClearButton && |
432 | + !control.readOnly && |
433 | + (control.activeFocus && ((editor.text != "") || editor.inputMethodComposing)) |
434 | + |
435 | + Icon { |
436 | + id: icon |
437 | + anchors.verticalCenter: parent.verticalCenter |
438 | + width: units.gu(2.5) |
439 | + height: width |
440 | + // use icon from icon-theme |
441 | + name: control.hasClearButton && !control.readOnly ? "clear-search" : "" |
442 | + } |
443 | + |
444 | + onClicked: editor.text = "" |
445 | + } |
446 | + |
447 | Component.onCompleted: { |
448 | editor.accepted.connect(control.accepted); |
449 | cursorPosition = 0; |
450 | |
451 | === modified file 'modules/Ubuntu/Components/TextInputPopover.qml' |
452 | --- modules/Ubuntu/Components/TextInputPopover.qml 2014-08-31 19:24:19 +0000 |
453 | +++ modules/Ubuntu/Components/TextInputPopover.qml 2014-11-10 18:31:06 +0000 |
454 | @@ -26,6 +26,7 @@ |
455 | Action { |
456 | text: i18n.dtr('ubuntu-ui-toolkit', "Select All") |
457 | iconName: "edit-select-all" |
458 | + enabled: target.text !== "" |
459 | visible: target && target.selectedText === "" |
460 | onTriggered: target.selectAll() |
461 | }, |
462 | @@ -34,25 +35,40 @@ |
463 | iconName: "edit-cut" |
464 | // If paste/editing is not possible, then disable also "Cut" operation |
465 | // It is applicable for ReadOnly's TextFields and TextAreas |
466 | - enabled: target && target.selectedText !== "" && target.canPaste |
467 | + enabled: target && target.selectedText !== "" && !target.readOnly |
468 | visible: target.selectedText !== "" |
469 | - onTriggered: target.cut() |
470 | + onTriggered: { |
471 | + PopupUtils.close(popover); |
472 | + target.cut(); |
473 | + } |
474 | }, |
475 | Action { |
476 | text: i18n.dtr('ubuntu-ui-toolkit', "Copy") |
477 | iconName: "edit-copy" |
478 | enabled: target && target.selectedText !== "" |
479 | visible: target.selectedText !== "" |
480 | - onTriggered: target.copy() |
481 | + onTriggered: { |
482 | + PopupUtils.close(popover); |
483 | + target.copy(); |
484 | + } |
485 | }, |
486 | Action { |
487 | text: i18n.dtr('ubuntu-ui-toolkit', "Paste") |
488 | iconName: "edit-paste" |
489 | enabled: target && target.canPaste |
490 | - onTriggered: target.paste() |
491 | + onTriggered: { |
492 | + PopupUtils.close(popover); |
493 | + target.paste(); |
494 | + } |
495 | } |
496 | ] |
497 | |
498 | + // removes hide animation |
499 | + function hide() { |
500 | + popover.visible = false; |
501 | + } |
502 | + |
503 | + autoClose: false |
504 | contentHeight: row.childrenRect.height |
505 | contentWidth: row.childrenRect.width |
506 | Row { |
507 | @@ -73,7 +89,6 @@ |
508 | height: units.gu(6) |
509 | action: actions[modelData] |
510 | style: Theme.createStyleComponent("ToolbarButtonStyle.qml", button) |
511 | - onClicked: popover.hide() |
512 | } |
513 | } |
514 | } |
515 | |
516 | === modified file 'modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml' |
517 | --- modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml 2014-10-07 15:21:11 +0000 |
518 | +++ modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml 2014-11-10 18:31:06 +0000 |
519 | @@ -53,10 +53,11 @@ |
520 | Component { |
521 | id: delegate |
522 | Rectangle { |
523 | + objectName: "text_cursor_style_" + styledItem.positionProperty |
524 | width: cursorWidth |
525 | // FIXME: Extend the palette and use palette values here |
526 | color: UbuntuColors.blue |
527 | - visible: blinkTimer.timerShowCursor |
528 | + visible: blinkTimer.timerShowCursor || !blinkTimer.running |
529 | Timer { |
530 | id: blinkTimer |
531 | interval: cursorStyle.cursorVisibleTimeout |
532 | |
533 | === modified file 'modules/Ubuntu/Components/plugin/ucmousefilters.cpp' |
534 | --- modules/Ubuntu/Components/plugin/ucmousefilters.cpp 2014-04-25 12:53:58 +0000 |
535 | +++ modules/Ubuntu/Components/plugin/ucmousefilters.cpp 2014-11-10 18:31:06 +0000 |
536 | @@ -784,6 +784,7 @@ |
537 | m_owner->installEventFilter(this); |
538 | } else { |
539 | m_owner->removeEventFilter(this); |
540 | + m_pressAndHoldTimer.stop(); |
541 | } |
542 | Q_EMIT enabledChanged(); |
543 | } |
544 | |
545 | === modified file 'tests/autopilot/ubuntuuitoolkit/base.py' |
546 | --- tests/autopilot/ubuntuuitoolkit/base.py 2014-09-17 16:06:35 +0000 |
547 | +++ tests/autopilot/ubuntuuitoolkit/base.py 2014-11-10 18:31:06 +0000 |
548 | @@ -21,6 +21,9 @@ |
549 | import os |
550 | import subprocess |
551 | import ubuntuuitoolkit |
552 | +from autopilot.introspection import dbus |
553 | +from autopilot.matchers import Eventually |
554 | +from testtools.matchers import Equals |
555 | |
556 | from autopilot import ( |
557 | input, |
558 | @@ -80,3 +83,16 @@ |
559 | return input.Mouse |
560 | else: |
561 | return input.Touch |
562 | + |
563 | + def _assert_not_visible(self, type_name='*', **kwargs): |
564 | + """Confirm that an object is hidden. |
565 | + |
566 | + Internally this means asserting that selecting the object fails. |
567 | + """ |
568 | + try: |
569 | + object = self.main_view.select_single(type_name, **kwargs) |
570 | + # object.visible is always True if the select succeeds |
571 | + self.assertThat(object.visible, Eventually(Equals(False))) |
572 | + except dbus.StateNotFoundError: |
573 | + # Caret can't be selected because it's hidden |
574 | + pass |
575 | |
576 | === modified file 'tests/autopilot/ubuntuuitoolkit/tests/components/test_textinput.py' |
577 | --- tests/autopilot/ubuntuuitoolkit/tests/components/test_textinput.py 2014-09-22 07:19:56 +0000 |
578 | +++ tests/autopilot/ubuntuuitoolkit/tests/components/test_textinput.py 2014-11-10 18:31:06 +0000 |
579 | @@ -17,8 +17,11 @@ |
580 | """Tests for the Ubuntu UI Toolkit Header component.""" |
581 | |
582 | import os |
583 | +import testtools |
584 | +from time import sleep |
585 | |
586 | -from autopilot.introspection import dbus |
587 | +from autopilot.input._common import get_center_point |
588 | +from autopilot import platform |
589 | |
590 | from ubuntuuitoolkit import tests |
591 | |
592 | @@ -57,19 +60,11 @@ |
593 | self.assertFalse(self.textfield.focus) |
594 | |
595 | def test_caret_visible_on_focus(self): |
596 | - try: |
597 | - cursor = self.main_view.select_single( |
598 | - objectName='text_cursor_style_caret_cursorPosition') |
599 | - # cursor.visible is always True if the select succeeds |
600 | - self.assertFalse(cursor.visible) |
601 | - except dbus.StateNotFoundError: |
602 | - # Caret can't be selected because it's hidden |
603 | - pass |
604 | - |
605 | + cursorName = 'text_cursor_style_caret_cursorPosition' |
606 | + self._assert_not_visible(objectName=cursorName) |
607 | self.pointing_device.click_object(self.textfield) |
608 | self.assertTrue(self.textfield.focus) |
609 | - cursor = self.main_view.select_single( |
610 | - objectName='text_cursor_style_caret_cursorPosition') |
611 | + cursor = self.main_view.select_single(objectName=cursorName) |
612 | self.assertTrue(cursor.visible) |
613 | |
614 | def test_caret_hide_while_typing(self): |
615 | @@ -96,3 +91,91 @@ |
616 | cursor = self.main_view.select_single( |
617 | objectName='text_cursor_style_caret_selectionEnd') |
618 | self.assertTrue(cursor.visible) |
619 | + |
620 | + |
621 | +class InsertModeTextInputTestCase(tests.QMLFileAppTestCase): |
622 | + |
623 | + path = os.path.abspath(__file__) |
624 | + dir_path = os.path.dirname(path) |
625 | + textfield_qml_file_path = os.path.join( |
626 | + dir_path, 'test_textinput.textfield.qml') |
627 | + textarea_qml_file_path = os.path.join( |
628 | + dir_path, 'test_textinput.textarea.qml') |
629 | + customfield_qml_file_path = os.path.join( |
630 | + dir_path, 'test_textinput.textfield_custom.qml') |
631 | + |
632 | + scenarios = [ |
633 | + ('textfield', |
634 | + dict(test_qml_file_path=textfield_qml_file_path, |
635 | + objectName='textfield')), |
636 | + ('textarea', |
637 | + dict(test_qml_file_path=textarea_qml_file_path, |
638 | + objectName='textarea')), |
639 | + ('customfield', |
640 | + dict(test_qml_file_path=customfield_qml_file_path, |
641 | + objectName='textfield')), |
642 | + ] |
643 | + |
644 | + def get_command_line(self, command_line): |
645 | + command_line.append('-touch') |
646 | + return command_line |
647 | + |
648 | + def setUp(self): |
649 | + super(InsertModeTextInputTestCase, self).setUp() |
650 | + self.textfield = self.main_view.select_single( |
651 | + objectName=self.objectName) |
652 | + self.assertFalse(self.textfield.focus) |
653 | + |
654 | + def assert_buttons(self, texts): |
655 | + popover = self.main_view.get_text_input_context_menu( |
656 | + 'text_input_contextmenu') |
657 | + for text in texts: |
658 | + button = popover._get_button(text) |
659 | + self.assertTrue(button.visible) |
660 | + |
661 | + def assert_discard_popover(self): |
662 | + # Discard popover by tap |
663 | + self.pointing_device.move( |
664 | + self.textfield.globalRect.x + self.textfield.width * 0.7, |
665 | + self.textfield.globalRect.y + self.textfield.height / 10) |
666 | + self.pointing_device.click() |
667 | + |
668 | + self._assert_not_visible(objectName='text_input_contextmenu') |
669 | + |
670 | + @testtools.skipIf(platform.model() == 'Desktop', 'Touch only') |
671 | + def test_popover_visible_after_tapping_caret(self): |
672 | + # Insert Mode |
673 | + self.pointing_device.click_object(self.textfield) |
674 | + sleep(1) |
675 | + cursor = self.main_view.select_single( |
676 | + objectName='text_cursor_style_cursorPosition') |
677 | + self.pointing_device.click_object(cursor) |
678 | + self.assert_buttons(['Select All', 'Paste']) |
679 | + self.assert_discard_popover() |
680 | + |
681 | + @testtools.skipIf(platform.model() == 'Desktop', 'Touch only') |
682 | + def test_popover_visible_after_dragging_caret(self): |
683 | + # Insert Mode |
684 | + self.pointing_device.click_object(self.textfield) |
685 | + self.textfield.keyboard.type('Lorem ipsum') |
686 | + #self.pointing_device.click_object(self.textfield) |
687 | + cursor = self.main_view.select_single( |
688 | + objectName='text_cursor_style_cursorPosition') |
689 | + x, y = get_center_point(cursor) |
690 | + self.pointing_device.drag(x, y, 0, y) |
691 | + self.assert_buttons(['Select All', 'Paste']) |
692 | + self.assert_discard_popover() |
693 | + |
694 | + @testtools.skipIf(platform.model() == 'Desktop', 'Touch only') |
695 | + def test_popover_visible_after_selecting(self): |
696 | + # Select Mode |
697 | + self.pointing_device.click_object(self.textfield) |
698 | + self.textfield.keyboard.type('Lorem ipsum') |
699 | + self.pointing_device.move( |
700 | + self.textfield.globalRect.x + self.textfield.width / 8, |
701 | + self.textfield.globalRect.y + self.textfield.height / 2) |
702 | + # Long press to select a word |
703 | + self.pointing_device.click() |
704 | + self.pointing_device.click() |
705 | + self.assert_buttons(['Cut', 'Copy', 'Paste']) |
706 | + self.assert_discard_popover() |
707 | |
708 | === modified file 'tests/unit_x11/tst_components/tst_textinput_common.qml' |
709 | --- tests/unit_x11/tst_components/tst_textinput_common.qml 2014-07-13 07:19:15 +0000 |
710 | +++ tests/unit_x11/tst_components/tst_textinput_common.qml 2014-11-10 18:31:06 +0000 |
711 | @@ -175,38 +175,6 @@ |
712 | cursorRectSpy.target = null; |
713 | } |
714 | |
715 | - function test_press_and_hold_does_nothing_data() { |
716 | - return [ |
717 | - {tag: "TextField", input: textField, withTextSelected: false}, |
718 | - {tag: "TextArea", input: textArea, withTextSelected: false}, |
719 | - {tag: "TextField", input: textField, withTextSelected: true}, |
720 | - {tag: "TextArea", input: textArea, withTextSelected: true}, |
721 | - ]; |
722 | - } |
723 | - |
724 | - function test_press_and_hold_does_nothing(data) { |
725 | - var handler = findChild(data.input, "input_handler"); |
726 | - popupSpy.target = handler; |
727 | - |
728 | - data.input.focus = true; |
729 | - if (data.withTextSelected) { |
730 | - // select the first 20 characters |
731 | - data.input.select(0, 20); |
732 | - } |
733 | - |
734 | - // press and hold over selected text |
735 | - mouseLongPress(data.input, units.gu(7), y); |
736 | - waitForRendering(data.input); |
737 | - expectFailContinue(data.tag, "Should not open popover"); |
738 | - popupSpy.wait(400); |
739 | - |
740 | - if (data.withTextSelected) { |
741 | - // text selection must not be cleared |
742 | - verify(data.input.selectedText !== "", "Selected text cleared!"); |
743 | - } |
744 | - mouseRelease(data.input, units.gu(7), y); |
745 | - } |
746 | - |
747 | function test_scroll_when_not_focused_data() { |
748 | return [ |
749 | // dx and dy are in eights of a degree; see QWheelEvent::angleDelta() for more details. |
750 | @@ -274,7 +242,7 @@ |
751 | {tag: "TextField click on selection", input: textField, selectChars: 10, clickPos: Qt.point(10, textField.height / 2)}, |
752 | {tag: "TextArea click on selection", input: textArea, selectChars: 40, clickPos: Qt.point(20, 20)}, |
753 | {tag: "TextField click beside selection", input: textField, selectChars: 5, clickPos: Qt.point(textField.width / 2, textField.height / 2)}, |
754 | - {tag: "TextArea click beside selection", input: textArea, selectChars: 5, clickPos: Qt.point(textArea.width / 2, textArea.height / 2)}, |
755 | + {tag: "TextArea click beside selection", input: textArea, selectChars: 1, clickPos: Qt.point(textArea.width / 2, textArea.height / 2)}, |
756 | ]; |
757 | } |
758 | function test_clear_selection_on_click(data) { |
759 | |
760 | === modified file 'tests/unit_x11/tst_components/tst_textinput_touch.qml' |
761 | --- tests/unit_x11/tst_components/tst_textinput_touch.qml 2014-08-26 10:36:07 +0000 |
762 | +++ tests/unit_x11/tst_components/tst_textinput_touch.qml 2014-11-10 18:31:06 +0000 |
763 | @@ -223,7 +223,7 @@ |
764 | return [ |
765 | {tag: "TextField", input: textField, initialCursorPosition: 0, cursorName: "selectionEnd", delta: guPoint(10, 0)}, |
766 | {tag: "TextArea", input: textArea, initialCursorPosition: 0, cursorName: "selectionEnd", delta: guPoint(10, 5)}, |
767 | - {tag: "TextField", input: textField, initialCursorPosition: 50, cursorName: "selectionStart", delta: guPoint(-10, 0)}, |
768 | + {tag: "TextField", input: textField, initialCursorPosition: 48, cursorName: "selectionStart", delta: guPoint(-10, 0)}, |
769 | {tag: "TextArea", input: textArea, initialCursorPosition: 50, cursorName: "selectionStart", delta: guPoint(-20, -5)}, |
770 | ]; |
771 | } |
772 | @@ -268,7 +268,6 @@ |
773 | function test_0_drag_autosizing_textarea_drags_parent_flickable_data() { |
774 | return [ |
775 | {tag: "when inactive", focused: false }, |
776 | - {tag: "when active", focused: true }, |
777 | ]; |
778 | } |
779 | function test_0_drag_autosizing_textarea_drags_parent_flickable(data) { |
Not ready for review yet.