Merge lp:~zsombi/ubuntu-ui-toolkit/textinput-carets into lp:ubuntu-ui-toolkit/staging
- textinput-carets
- Merge into staging
Status: | Merged |
---|---|
Approved by: | Zsombor Egri |
Approved revision: | 1036 |
Merged at revision: | 1037 |
Proposed branch: | lp:~zsombi/ubuntu-ui-toolkit/textinput-carets |
Merge into: | lp:ubuntu-ui-toolkit/staging |
Prerequisite: | lp:~zsombi/ubuntu-ui-toolkit/textinputs-rightclick |
Diff against target: |
1322 lines (+808/-173) 16 files modified
components.api (+1/-0) modules/Ubuntu/Components/InputHandler.qml (+107/-5) modules/Ubuntu/Components/TextArea.qml (+10/-72) modules/Ubuntu/Components/TextCursor.qml (+235/-26) modules/Ubuntu/Components/TextField.qml (+11/-21) modules/Ubuntu/Components/Themes/Ambiance/TextAreaStyle.qml (+16/-1) modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml (+58/-40) modules/Ubuntu/Components/Themes/Ambiance/TextFieldStyle.qml (+1/-0) modules/Ubuntu/Components/Themes/Ambiance/TextSelectionEndCursorStyle.qml (+66/-0) modules/Ubuntu/Components/Themes/Ambiance/TextSelectionStartCursorStyle.qml (+66/-0) modules/Ubuntu/Components/Themes/Ambiance/qmldir (+3/-0) modules/Ubuntu/Test/UbuntuTestCase.qml (+25/-2) tests/resources/inputs/TextInputs.qml (+4/-0) tests/unit_x11/tst_components/tst_textarea.qml (+6/-5) tests/unit_x11/tst_components/tst_textfield.qml (+1/-1) tests/unit_x11/tst_components/tst_textinput_common.qml (+198/-0) |
To merge this branch: | bzr merge lp:~zsombi/ubuntu-ui-toolkit/textinput-carets |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Cris Dywan | Approve | ||
Review via email: mp+216665@code.launchpad.net |
Commit message
Text input caret handler visuals and functionality.
Description of the change
Cris Dywan (kalikiana) wrote : | # |
Zsombor Egri (zsombi) wrote : | # |
> The carrots actually do a nice job on the desktop enabling me to extend
> selections with just a mouse - ordinarily it would require ⇧Arrow.
>
> A single carrot is visible when there's no selection but can't be dragged.
The single caret is just to grab the cursor for easy positioning of it. It's not a selection caret. The selection cares get visible when there is selected text.
>
> If the selection is at the very start of the text field the first carrot is
> clipped.
The carets are clipped in the input area atm, so the selection beginning will be clipped yet. I'll try to get a similar image above the actual one, but unfortunately positioning the overlay caret is not that stable... And may require changes in the logic...
>
> 505 - Ubuntu.
> 506 +// Ubuntu.
>
> Accident?
Yes, good catch!
>
> 526 +// FIXME: stabilize API
>
> What does this mean? Could you make the comment more verbose or mention a bug?
Means that the API will be moved in Ubuntu.
Cris Dywan (kalikiana) wrote : | # |
> > The carrots actually do a nice job on the desktop enabling me to extend
> > selections with just a mouse - ordinarily it would require ⇧Arrow.
> >
> > A single carrot is visible when there's no selection but can't be dragged.
>
> The single caret is just to grab the cursor for easy positioning of it. It's
> not a selection caret. The selection cares get visible when there is selected
> text.
The problem with that is to start a selection you still have to aim freely, so there's no benefit in precisely moving the caret right now - unless there is going to be a way to long press/ right-click the carrot itself to start a selection or invoke the context menu.
> > If the selection is at the very start of the text field the first carrot is
> > clipped.
>
> The carets are clipped in the input area atm, so the selection beginning will
> be clipped yet. I'll try to get a similar image above the actual one, but
> unfortunately positioning the overlay caret is not that stable... And may
> require changes in the logic...
I could agree to filing a bug for it and leaving it as-is for now, it's not a deal breaker for typical use.
> >
> > 526 +// FIXME: stabilize API
> >
> > What does this mean? Could you make the comment more verbose or mention a
> bug?
>
> Means that the API will be moved in Ubuntu.
> everything done, and in a separate MR perhaps.
Maybe just leave that out of this branch then? Since it's not actually a bug or anything, more like something that belongs in a bug or blueprint.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1032
http://
Executed test runs:
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1033
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1034
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
Cris Dywan (kalikiana) wrote : | # |
> > > A single carrot is visible when there's no selection but can't be dragged.
> > The single caret is just to grab the cursor for easy positioning of it. It's
> > not a selection caret.
> The problem with that is to start a selection you still have to aim freely, so
> there's no benefit in precisely moving the caret right now - unless there is
> going to be a way to long press/ right-click the carrot itself to start a
> selection or invoke the context menu.
?
> > > at the very start of the text field the first carrot is clipped.
> > The carets are clipped in the input area atm
It would seem to be fixed now. Nice.
> > > 526 +// FIXME: stabilize API
> > > What does this mean? Could you make the comment more verbose
> > Means that the API will be moved in Ubuntu.
> > everything done, and in a separate MR perhaps.
> Maybe just leave that out of this branch then? Since it's not actually a bug
> or anything, more like something that belongs in a bug or blueprint.
?
From more testing I noticed, sometimes selecting text with the mouse gets ignored, even if you try a few times, and then it works again. Not sure what exactly causes it, it's hard to say if it's stuck or a timeout is catching it. I almost thought the last changes broke normal mouse selection :-(
Zsombor Egri (zsombi) wrote : | # |
> > > > A single carrot is visible when there's no selection but can't be
> dragged.
> > > The single caret is just to grab the cursor for easy positioning of it.
> It's
> > > not a selection caret.
> > The problem with that is to start a selection you still have to aim freely,
> so
> > there's no benefit in precisely moving the caret right now - unless there is
> > going to be a way to long press/ right-click the carrot itself to start a
> > selection or invoke the context menu.
>
> ?
So, first of all, the caret is a handler which is used to grab the cursor (both in edit and select modes). We need them in all the modes, not only in selection mode.
You need to be able to position the cursor pretty precisely if you want to insert some text at a given position. When you have a single caret, you only have the cursor, you are not in the selection mode yet. That caret moves the cursor in a way so you can see where it actually gets positioned.
>
> > > > at the very start of the text field the first carrot is clipped.
> > > The carets are clipped in the input area atm
>
> It would seem to be fixed now. Nice.
>
> > > > 526 +// FIXME: stabilize API
> > > > What does this mean? Could you make the comment more verbose
> > > Means that the API will be moved in Ubuntu.
> > > everything done, and in a separate MR perhaps.
> > Maybe just leave that out of this branch then? Since it's not actually a bug
> > or anything, more like something that belongs in a bug or blueprint.
>
> ?
FIXME comments we use not only to fix something, but to remember that there's more work to be done there. I can remove that of course, and I won't have the API stabilized in this MR for sure, but it is good to have extra reminders that these kind of tasks should also be done.
>
> From more testing I noticed, sometimes selecting text with the mouse gets
> ignored, even if you try a few times, and then it works again. Not sure what
> exactly causes it, it's hard to say if it's stuck or a timeout is catching it.
> I almost thought the last changes broke normal mouse selection :-(
Double tap? I noticed that because of the caret positioning, if you tap too close to the caret, the double tap will get stolen by the caret dragger... Perhaps I'd need to position the caret a bit closer to the bottom of the cursor so it doesn't interfere with the clicks...
Cris Dywan (kalikiana) wrote : | # |
Very nice to have the cursor take events now. And I can actually open the menu more precisely through the carrot in tricky cases with small letters.
As I double-checked, the ignored selections are not new in this branch, just depend a lot on the exact mouse movement, so let's not worry about it here.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1035
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Autolanding.
More details in the following jenkins job:
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
- 1036. By Zsombor Egri
-
staging merge
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1036
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'components.api' |
2 | --- components.api 2014-05-02 11:09:58 +0000 |
3 | +++ components.api 2014-05-06 12:22:48 +0000 |
4 | @@ -605,6 +605,7 @@ |
5 | TestCase |
6 | function findChild(obj,objectName) |
7 | function findInvisibleChild(obj,objectName) |
8 | + function findChildWithProperty(item, property, value) |
9 | function mouseMoveSlowly(item,x,y,dx,dy,steps,stepdelay) |
10 | function flick(item, x, y, dx, dy, pressTimeout, steps, button, modifiers, delay) |
11 | function mouseLongPress(item, x, y, button, modifiers, delay) |
12 | |
13 | === modified file 'modules/Ubuntu/Components/InputHandler.qml' |
14 | --- modules/Ubuntu/Components/InputHandler.qml 2014-05-01 12:15:29 +0000 |
15 | +++ modules/Ubuntu/Components/InputHandler.qml 2014-05-06 12:22:48 +0000 |
16 | @@ -29,7 +29,7 @@ |
17 | property Item main |
18 | // the input instance |
19 | property Item input |
20 | - // the Flickable holdiong the input instance |
21 | + // the Flickable holding the input instance |
22 | property Flickable flickable |
23 | // selection cursor mode |
24 | property bool selectionCursor: input && input.selectedText !== "" |
25 | @@ -38,7 +38,20 @@ |
26 | // property holding the selection mode timeout |
27 | property int selectionModeTimeout: 200 |
28 | |
29 | - // signal triggered when popup shoudl be opened |
30 | + // item filling the text input visible area, used to check teh caret handler |
31 | + // visibility |
32 | + property Item visibleArea: Item { |
33 | + parent: flickable |
34 | + anchors.fill: parent |
35 | + } |
36 | + |
37 | + // line size and spacing |
38 | + property real lineSpacing: units.dp(3) |
39 | + property real lineSize: input.font.pixelSize + lineSpacing |
40 | + // input x/y distance from the frame |
41 | + property point frameDistance: Qt.point(0,0) |
42 | + |
43 | + // signal triggered when popup should be opened |
44 | signal pressAndHold(int pos) |
45 | |
46 | function activateInput() { |
47 | @@ -80,9 +93,11 @@ |
48 | readonly property Flickable scroller: (scrollingDisabled && grandScroller) ? grandScroller : flickable |
49 | |
50 | // ensures the text cusrorRectangle is always in the internal Flickable's visible area |
51 | - function ensureVisible() |
52 | + function ensureVisible(rect) |
53 | { |
54 | - var rect = input.cursorRectangle; |
55 | + if (rect === undefined) { |
56 | + rect = input.cursorRectangle; |
57 | + } |
58 | if (flickable.moving || flickable.flicking) |
59 | return; |
60 | if (flickable.contentX >= rect.x) |
61 | @@ -94,9 +109,14 @@ |
62 | else if (flickable.contentY + flickable.height <= rect.y + rect.height) |
63 | flickable.contentY = rect.y + rect.height - flickable.height; |
64 | } |
65 | + // returns the cursor position from x,y pair |
66 | + function cursorPosition(x, y) { |
67 | + return singleLine ? input.positionAt(x, TextInput.CursorOnCharacter) : input.positionAt(x, y, TextInput.CursorOnCharacter); |
68 | + } |
69 | + |
70 | // returns the mouse position |
71 | function mousePosition(mouse) { |
72 | - return singleLine ? input.positionAt(mouse.x) : input.positionAt(mouse.x, mouse.y); |
73 | + return cursorPosition(mouse.x, mouse.y); |
74 | } |
75 | // checks whether the position is in the selected text |
76 | function positionInSelection(pos) { |
77 | @@ -165,6 +185,30 @@ |
78 | } |
79 | } |
80 | |
81 | + // moves the specified position, called by the cursor handler |
82 | + // positioner = "currentPosition/selectionStart/selectionEnd" |
83 | + function positionCaret(positioner, x, y) { |
84 | + if (positioner === "cursorPosition") { |
85 | + input[positioner] = cursorPosition(x, y); |
86 | + } else { |
87 | + var pos = cursorPosition(x, y); |
88 | + if (positioner === "selectionStart" && (pos < input.selectionEnd)) { |
89 | + input.select(pos, input.selectionEnd); |
90 | + } else if (positioner === "selectionEnd" && (pos > input.selectionStart)) { |
91 | + input.select(input.selectionStart, pos); |
92 | + } |
93 | + } |
94 | + } |
95 | + |
96 | + // returns the styles for the cursors depending on the position property given |
97 | + function textCursorStyle(positionProperty) { |
98 | + switch (positionProperty) { |
99 | + case "cursorPosition": return main.__styleInstance.mainCursorStyle; |
100 | + case "selectionStart": return main.__styleInstance.selectionStartCursorStyle; |
101 | + case "selectionEnd": return main.__styleInstance.selectionEndCursorStyle; |
102 | + } |
103 | + } |
104 | + |
105 | Component.onCompleted: { |
106 | state = (main.focus) ? "" : "inactive"; |
107 | } |
108 | @@ -313,4 +357,62 @@ |
109 | // trigger pressAndHold |
110 | onReleased: openContextMenu(mouse) |
111 | } |
112 | + |
113 | + // cursors to use when text is selected |
114 | + Connections { |
115 | + property Item selectionStartCursor: null |
116 | + property Item selectionEndCursor: null |
117 | + target: input |
118 | + onSelectedTextChanged: { |
119 | + if (selectedText !== "" && input.cursorDelegate) { |
120 | + if (!selectionStartCursor) { |
121 | + selectionStartCursor = input.cursorDelegate.createObject( |
122 | + input, { |
123 | + "positionProperty": "selectionStart", |
124 | + "height": lineSize, |
125 | + "handler": inputHandler, |
126 | + } |
127 | + ); |
128 | + moveSelectionCursor(selectionStartCursor); |
129 | + } |
130 | + if (!selectionEndCursor) { |
131 | + selectionEndCursor = input.cursorDelegate.createObject( |
132 | + input, { |
133 | + "positionProperty": "selectionEnd", |
134 | + "height": lineSize, |
135 | + "handler": inputHandler, |
136 | + } |
137 | + ); |
138 | + moveSelectionCursor(selectionEndCursor); |
139 | + } |
140 | + } else { |
141 | + if (selectionStartCursor) { |
142 | + selectionStartCursor.destroy(); |
143 | + selectionStartCursor = null; |
144 | + } |
145 | + if (selectionEndCursor) { |
146 | + selectionEndCursor.destroy(); |
147 | + selectionEndCursor = null; |
148 | + } |
149 | + } |
150 | + } |
151 | + onSelectionStartChanged: moveSelectionCursor(selectionStartCursor, true); |
152 | + onSelectionEndChanged: moveSelectionCursor(selectionEndCursor, true); |
153 | + |
154 | + function moveSelectionCursor(cursor, updateProperty) { |
155 | + if (!cursor) { |
156 | + return; |
157 | + } |
158 | + // workaround for https://bugreports.qt-project.org/browse/QTBUG-38704 |
159 | + // selectedTextChanged signal is not emitted for TextEdit when selectByMouse is false |
160 | + if (updateProperty && QuickUtils.className(input) === "QQuickTextEdit") { |
161 | + input.selectedTextChanged(); |
162 | + } |
163 | + |
164 | + var pos = input.positionToRectangle(input[cursor.positionProperty]); |
165 | + cursor.x = pos.x; |
166 | + cursor.y = pos.y; |
167 | + ensureVisible(pos); |
168 | + } |
169 | + } |
170 | } |
171 | |
172 | === modified file 'modules/Ubuntu/Components/TextArea.qml' |
173 | --- modules/Ubuntu/Components/TextArea.qml 2014-04-28 06:48:28 +0000 |
174 | +++ modules/Ubuntu/Components/TextArea.qml 2014-05-06 12:22:48 +0000 |
175 | @@ -158,7 +158,7 @@ |
176 | area defined in the current theme. The default value is the same as the visible |
177 | input area's width. |
178 | */ |
179 | - property real contentWidth: internal.inputAreaWidth |
180 | + property real contentWidth: control.width - 2 * internal.frameSpacing |
181 | |
182 | /*! |
183 | The property folds the height of the text editing content. This can be equal or |
184 | @@ -166,7 +166,7 @@ |
185 | area defined in the current theme. The default value is the same as the visible |
186 | input area's height. |
187 | */ |
188 | - property real contentHeight: internal.inputAreaHeight |
189 | + property real contentHeight: control.height - 2 * internal.frameSpacing |
190 | |
191 | /*! |
192 | The property overrides the default popover of the TextArea. When set, the |
193 | @@ -233,10 +233,8 @@ |
194 | |
195 | Note that the root item of the delegate component must be a QQuickItem or |
196 | QQuickItem derived item. |
197 | - |
198 | - \qmlproperty Component cursorDelegate |
199 | */ |
200 | - property alias cursorDelegate: editor.cursorDelegate |
201 | + property Component cursorDelegate: null |
202 | |
203 | /*! |
204 | The position of the cursor in the TextArea. |
205 | @@ -731,7 +729,6 @@ |
206 | /*!\internal - to remove warnings */ |
207 | Component.onCompleted: { |
208 | editor.linkActivated.connect(control.linkActivated); |
209 | - internal.prevShowCursor = control.cursorVisible; |
210 | } |
211 | |
212 | // activation area on mouse click |
213 | @@ -756,15 +753,6 @@ |
214 | control.focus = false; |
215 | } |
216 | |
217 | - /*!\internal */ |
218 | - onContentWidthChanged: internal.inputAreaWidth = control.contentWidth |
219 | - /*!\internal */ |
220 | - onContentHeightChanged: internal.inputAreaHeight = control.contentHeight |
221 | - /*!\internal */ |
222 | - onWidthChanged: internal.inputAreaWidth = control.width - 2 * internal.frameSpacing |
223 | - /*!\internal */ |
224 | - onHeightChanged: internal.inputAreaHeight = control.height - 2 * internal.frameSpacing |
225 | - |
226 | LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft |
227 | LayoutMirroring.childrenInherit: true |
228 | |
229 | @@ -772,32 +760,12 @@ |
230 | id: internal |
231 | // public property locals enabling aliasing |
232 | property string displayText: editor.getText(0, editor.length) |
233 | - property real lineSpacing: units.dp(3) |
234 | property real frameSpacing: control.__styleInstance.frameSpacing |
235 | - property real lineSize: editor.font.pixelSize + lineSpacing |
236 | property real minimumSize: units.gu(4) |
237 | - property real inputAreaWidth: control.width - 2 * frameSpacing |
238 | - property real inputAreaHeight: control.height - 2 * frameSpacing |
239 | - //selection properties |
240 | - property bool prevShowCursor |
241 | - |
242 | - function toggleSelectionCursors(show) |
243 | - { |
244 | - if (!show) { |
245 | - leftCursorLoader.sourceComponent = undefined; |
246 | - rightCursorLoader.sourceComponent = undefined; |
247 | - editor.cursorVisible = prevShowCursor; |
248 | - } else { |
249 | - prevShowCursor = editor.cursorVisible; |
250 | - editor.cursorVisible = false; |
251 | - leftCursorLoader.sourceComponent = cursor; |
252 | - rightCursorLoader.sourceComponent = cursor; |
253 | - } |
254 | - } |
255 | |
256 | function linesHeight(lines) |
257 | { |
258 | - var lineHeight = editor.font.pixelSize * lines + lineSpacing * lines |
259 | + var lineHeight = editor.font.pixelSize * lines + inputHandler.lineSpacing * lines |
260 | return lineHeight + 2 * frameSpacing; |
261 | } |
262 | |
263 | @@ -829,39 +797,6 @@ |
264 | } |
265 | Keys.onReleased: event.accepted = (event.key === Qt.Key_Enter) || (event.key === Qt.Key_Return) |
266 | |
267 | - // cursor is FIXME: move in a separate element and align with TextField |
268 | - Component { |
269 | - id: cursor |
270 | - TextCursor { |
271 | - id: cursorItem |
272 | - editorItem: control |
273 | - height: internal.lineSize |
274 | - popover: control.popover |
275 | - visible: editor.cursorVisible |
276 | - |
277 | - Component.onCompleted: inputHandler.pressAndHold.connect(cursorItem.openPopover) |
278 | - } |
279 | - } |
280 | - // selection cursor loader |
281 | - Loader { |
282 | - id: leftCursorLoader |
283 | - onStatusChanged: { |
284 | - if (status == Loader.Ready && item) { |
285 | - item.positionProperty = "selectionStart"; |
286 | - item.parent = editor; |
287 | - } |
288 | - } |
289 | - } |
290 | - Loader { |
291 | - id: rightCursorLoader |
292 | - onStatusChanged: { |
293 | - if (status == Loader.Ready && item) { |
294 | - item.positionProperty = "selectionEnd"; |
295 | - item.parent = editor; |
296 | - } |
297 | - } |
298 | - } |
299 | - |
300 | // holding default values |
301 | Label { id: fontHolder } |
302 | |
303 | @@ -911,13 +846,15 @@ |
304 | readOnly: false |
305 | id: editor |
306 | focus: true |
307 | - width: internal.inputAreaWidth |
308 | - height: Math.max(internal.inputAreaHeight, editor.contentHeight) |
309 | + width: control.contentWidth |
310 | + height: Math.max(control.contentHeight, editor.contentHeight) |
311 | wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere |
312 | mouseSelectionMode: TextEdit.SelectWords |
313 | selectByMouse: false |
314 | activeFocusOnPress: false |
315 | - cursorDelegate: cursor |
316 | + cursorDelegate: TextCursor { |
317 | + handler: inputHandler |
318 | + } |
319 | color: control.__styleInstance.color |
320 | selectedTextColor: Theme.palette.selected.foregroundText |
321 | selectionColor: Theme.palette.selected.foreground |
322 | @@ -937,6 +874,7 @@ |
323 | input: editor |
324 | flickable: flicker |
325 | selectionModeTimeout: control.__styleInstance.selectionModeTimeout |
326 | + frameDistance: Qt.point(internal.frameSpacing, internal.frameSpacing) |
327 | } |
328 | } |
329 | } |
330 | |
331 | === modified file 'modules/Ubuntu/Components/TextCursor.qml' |
332 | --- modules/Ubuntu/Components/TextCursor.qml 2014-04-28 06:48:28 +0000 |
333 | +++ modules/Ubuntu/Components/TextCursor.qml 2014-05-06 12:22:48 +0000 |
334 | @@ -15,48 +15,257 @@ |
335 | */ |
336 | |
337 | import QtQuick 2.0 |
338 | +import Ubuntu.Components 1.1 as Ubuntu |
339 | import Ubuntu.Components.Popups 1.0 |
340 | |
341 | -StyledItem { |
342 | +Ubuntu.StyledItem { |
343 | id: cursorItem |
344 | |
345 | width: units.dp(1) |
346 | |
347 | /* |
348 | - Property holding the text input item instance. |
349 | - */ |
350 | - property var editorItem |
351 | - |
352 | - /* |
353 | Property holding the text input's custor position property. Can be one of |
354 | the following ones: cursorPosition, selectionStart and selectionEnd. |
355 | */ |
356 | property string positionProperty: "cursorPosition" |
357 | |
358 | /* |
359 | - The property contains the custom popover to be shown. |
360 | - */ |
361 | - property var popover |
362 | - |
363 | + Input handler instance |
364 | + */ |
365 | + property InputHandler handler |
366 | + |
367 | + /* |
368 | + Cursor delegate used. This is the visual component from the main |
369 | + */ |
370 | + readonly property Component cursorDelegate: handler.main.cursorDelegate ? |
371 | + handler.main.cursorDelegate : |
372 | + __styleInstance.cursorDelegate |
373 | + |
374 | + // depending on the positionProperty, we chose different styles |
375 | + style: Theme.createStyleComponent(handler.textCursorStyle(positionProperty), cursorItem); |
376 | + |
377 | + //Caret instance from the style. |
378 | + property Item caret: __styleInstance.caret |
379 | + property real caretX: caret ? caret.x : 0 |
380 | + property real caretY: caret ? caret.y : 0 |
381 | + |
382 | + // returns the mapped cursor position to a position relative to the main component |
383 | + function mappedCursorPosition(pos) { |
384 | + return cursorItem[pos] + handler.frameDistance[pos] - handler.flickable["content"+pos.toUpperCase()]; |
385 | + } |
386 | /* |
387 | The function opens the text input popover setting the text cursor as caller. |
388 | */ |
389 | + Connections { |
390 | + target: inputHandler |
391 | + onPressAndHold: openPopover() |
392 | + } |
393 | + |
394 | function openPopover() { |
395 | - if (!visible) |
396 | + if (!visible || opacity === 0.0 || dragger.drag.active) { |
397 | return; |
398 | - if (popover === undefined) { |
399 | - // open the default one |
400 | - PopupUtils.open(Qt.resolvedUrl("TextInputPopover.qml"), cursorItem, |
401 | - { |
402 | - "target": editorItem |
403 | - }) |
404 | - } else { |
405 | - PopupUtils.open(popover, cursorItem, |
406 | - { |
407 | - "target": editorItem |
408 | - }) |
409 | - } |
410 | - } |
411 | - |
412 | - style: Theme.createStyleComponent("TextCursorStyle.qml", cursorItem) |
413 | + } |
414 | + // open context menu only for cursorPosition or selectionEnd |
415 | + if (positionProperty !== "selectionStart") { |
416 | + if (handler.main.popover === undefined) { |
417 | + // open the default one |
418 | + PopupUtils.open(Qt.resolvedUrl("TextInputPopover.qml"), cursorItem, |
419 | + { |
420 | + "target": handler.main |
421 | + }) |
422 | + } else { |
423 | + PopupUtils.open(handler.main.popover, cursorItem, |
424 | + { |
425 | + "target": handler.main |
426 | + }) |
427 | + } |
428 | + } |
429 | + } |
430 | + |
431 | + visible: handler.main.cursorVisible |
432 | + // change opacity to 0 if text is selected and the positionProperty is cursorPosition |
433 | + // note: we should not touch visibility as cursorVisible alters that! |
434 | + opacity: (positionProperty === "cursorPosition") && (handler.main.selectedText !== "") ? 0.0 : 1.0 |
435 | + |
436 | + // cursor visual loader |
437 | + Loader { |
438 | + id: cursorLoader |
439 | + sourceComponent: cursorDelegate |
440 | + height: parent.height |
441 | + onItemChanged: { |
442 | + if (item) { |
443 | + cursorItem.width = item.width; |
444 | + } |
445 | + } |
446 | + // bind the cursor height as it may change depending on the text size |
447 | + Binding { |
448 | + target: cursorLoader.item |
449 | + property: "height" |
450 | + value: cursorLoader.height |
451 | + when: cursorLoader.item |
452 | + } |
453 | + } |
454 | + |
455 | + /* |
456 | + * Handle pressAndHold as well as right clicks when pressed around the |
457 | + */ |
458 | + MouseArea { |
459 | + anchors { |
460 | + fill: parent |
461 | + margins: -units.dp(4) |
462 | + } |
463 | + acceptedButtons: Qt.LeftButon | Qt.RightButton |
464 | + preventStealing: false |
465 | + onPressAndHold: openPopover() |
466 | + onClicked: if (mouse.button === Qt.RightButton) openPopover() |
467 | + } |
468 | + |
469 | + /* |
470 | + * Caret dragging handling. We need a separate item which is dragged along the |
471 | + * component's area, which can move freely and not attached to the caret itself. |
472 | + * This area will then be used to update the caret position. |
473 | + */ |
474 | + onXChanged: if (draggedItem.state === "") draggedItem.moveToCaret() |
475 | + onYChanged: if (draggedItem.state === "") draggedItem.moveToCaret() |
476 | + Component.onCompleted: draggedItem.moveToCaret() |
477 | + |
478 | + //dragged item |
479 | + Item { |
480 | + id: draggedItem |
481 | + objectName: cursorItem.positionProperty + "_draggeditem" |
482 | + width: caret ? caret.width : 0 |
483 | + height: caret ? caret.height : 0 |
484 | + parent: handler.main |
485 | + visible: cursorItem.visible && (cursorItem.opacity > 0.0) |
486 | + |
487 | + // when the dragging ends, reposition the dragger back to caret |
488 | + onStateChanged: { |
489 | + if (state === "") { |
490 | + draggedItem.moveToCaret(); |
491 | + } |
492 | + } |
493 | + |
494 | + /* |
495 | + Mouse area to turn on dragging or selection mode when pressed |
496 | + on the handler area. The drag mode is turned off when the drag |
497 | + gets inactive or when the LeftButton is released. |
498 | + */ |
499 | + MouseArea { |
500 | + objectName: cursorItem.positionProperty + "_activator" |
501 | + anchors.fill: parent |
502 | + acceptedButtons: Qt.LeftButton | Qt.RightButton |
503 | + preventStealing: true |
504 | + enabled: parent.width && parent.height |
505 | + |
506 | + onPressed: { |
507 | + draggedItem.moveToCaret(mouse.x, mouse.y); |
508 | + draggedItem.state = "dragging"; |
509 | + } |
510 | + onPressAndHold: openPopover() |
511 | + onClicked: if (mouse.button === Qt.RightButton) openPopover() |
512 | + Ubuntu.Mouse.forwardTo: [dragger] |
513 | + /* |
514 | + As we are forwarding the events to the upper mouse area, the release |
515 | + will not get into the normal MosueArea onRelease signal as the preventStealing |
516 | + will not have any effect on the handling. However due to the mouse |
517 | + filter's nature, we will still be able to grab mouse events and we |
518 | + can stop dragging. We only handle the release in case the drag hasn't |
519 | + been active at all, otherwise the drag will not be deactivated and we |
520 | + will end up in a binding loop on the moveToCaret() next time the caret |
521 | + handler is grabbed. |
522 | + */ |
523 | + Ubuntu.Mouse.onReleased: { |
524 | + if (!dragger.drag.active) { |
525 | + draggedItem.state = ""; |
526 | + } |
527 | + } |
528 | + } |
529 | + |
530 | + // aligns the draggedItem to the caret and resets the dragger |
531 | + function moveToCaret(cx, cy) { |
532 | + if (cx === undefined && cy === undefined) { |
533 | + cx = mappedCursorPosition("x") + caretX; |
534 | + cy = mappedCursorPosition("y") + caretY; |
535 | + } else { |
536 | + // move mouse position to caret |
537 | + cx += draggedItem.x; |
538 | + cy += draggedItem.y; |
539 | + } |
540 | + |
541 | + draggedItem.x = cx; |
542 | + draggedItem.y = cy; |
543 | + dragger.resetDrag(); |
544 | + } |
545 | + // positions caret to the dragged position |
546 | + function positionCaret() { |
547 | + if (dragger.drag.active) { |
548 | + handler.positionCaret(positionProperty, |
549 | + dragger.thumbStartX + dragger.dragAmountX + handler.flickable.contentX, |
550 | + dragger.thumbStartY + dragger.dragAmountY + handler.flickable.contentY); |
551 | + } |
552 | + } |
553 | + } |
554 | + MouseArea { |
555 | + id: dragger |
556 | + objectName: cursorItem.positionProperty + "_dragger" |
557 | + // fill the entire component area |
558 | + parent: handler.main |
559 | + anchors.fill: parent |
560 | + hoverEnabled: true |
561 | + preventStealing: drag.active |
562 | + enabled: draggedItem.enabled && draggedItem.state === "dragging" |
563 | + |
564 | + property int thumbStartX |
565 | + property int dragStartX |
566 | + property int dragAmountX: dragger.drag.target.x - dragStartX |
567 | + property int thumbStartY |
568 | + property int dragStartY |
569 | + property int dragAmountY: dragger.drag.target.y - dragStartY |
570 | + |
571 | + function resetDrag() { |
572 | + thumbStartX = mappedCursorPosition("x"); |
573 | + thumbStartY = mappedCursorPosition("y"); |
574 | + dragStartX = drag.target.x; |
575 | + dragStartY = drag.target.y; |
576 | + } |
577 | + |
578 | + // do not set minimum/maximum so we can drag outside of the Flickable area |
579 | + drag { |
580 | + target: draggedItem |
581 | + axis: handler.singleLine ? Drag.XAxis : Drag.XAndYAxis |
582 | + // deactivate dragging |
583 | + onActiveChanged: { |
584 | + if (!drag.active) { |
585 | + draggedItem.state = ""; |
586 | + } |
587 | + } |
588 | + } |
589 | + |
590 | + onDragAmountXChanged: draggedItem.positionCaret() |
591 | + onDragAmountYChanged: draggedItem.positionCaret() |
592 | + } |
593 | + |
594 | + // fake cursor, caret is reparented to it to avoid caret clipping |
595 | + Item { |
596 | + id: fakeCursor |
597 | + parent: handler.main |
598 | + width: cursorItem.width |
599 | + height: cursorItem.height |
600 | + //reparent caret to it |
601 | + Component.onCompleted: if (caret) caret.parent = fakeCursor |
602 | + |
603 | + x: mappedCursorPosition("x") |
604 | + y: mappedCursorPosition("y") |
605 | + |
606 | + // manual clipping: the caret should be visible only while the cursor's |
607 | + // top/bottom falls into the text area |
608 | + visible: { |
609 | + if (!caret || !cursorItem.visible || cursorItem.opacity < 1.0) |
610 | + return false; |
611 | + |
612 | + var leftTop = Qt.point(x - handler.frameDistance.x, y + handler.frameDistance.y + handler.lineSpacing); |
613 | + var rightBottom = Qt.point(x - handler.frameDistance.x, y + height - handler.frameDistance.y - handler.lineSpacing); |
614 | + return (handler.visibleArea.contains(leftTop) || handler.visibleArea.contains(rightBottom)); |
615 | + } |
616 | + } |
617 | } |
618 | |
619 | === modified file 'modules/Ubuntu/Components/TextField.qml' |
620 | --- modules/Ubuntu/Components/TextField.qml 2014-04-28 06:48:28 +0000 |
621 | +++ modules/Ubuntu/Components/TextField.qml 2014-05-06 12:22:48 +0000 |
622 | @@ -244,10 +244,8 @@ |
623 | |
624 | Note that the root item of the delegate component must be a QQuickItem or |
625 | QQuickItem derived item. |
626 | - |
627 | - \qmlproperty Component cursorDelegate |
628 | */ |
629 | - property alias cursorDelegate: editor.cursorDelegate |
630 | + property Component cursorDelegate: null |
631 | |
632 | /*! |
633 | The position of the cursor in the TextField. |
634 | @@ -910,20 +908,6 @@ |
635 | } |
636 | } |
637 | |
638 | - // cursor |
639 | - Component { |
640 | - id: cursor |
641 | - TextCursor { |
642 | - //FIXME: connect to root object once we have all TextInput properties exposed |
643 | - editorItem: editor |
644 | - height: internal.lineSize |
645 | - popover: control.popover |
646 | - visible: editor.cursorVisible |
647 | - |
648 | - Component.onCompleted: inputHandler.pressAndHold.connect(openPopover) |
649 | - } |
650 | - } |
651 | - |
652 | AbstractButton { |
653 | id: clearButton |
654 | objectName: "clear_button" |
655 | @@ -939,7 +923,6 @@ |
656 | (control.activeFocus && ((editor.text != "") || editor.inputMethodComposing)) |
657 | |
658 | Image { |
659 | - //anchors.fill: parent |
660 | anchors.verticalCenter: parent.verticalCenter |
661 | width: units.gu(3) |
662 | height: width |
663 | @@ -997,9 +980,9 @@ |
664 | // FocusScope will forward focus to this component |
665 | focus: true |
666 | anchors.verticalCenter: parent.verticalCenter |
667 | - // get the control's style |
668 | - clip: true |
669 | - cursorDelegate: cursor |
670 | + cursorDelegate: TextCursor { |
671 | + handler: inputHandler |
672 | + } |
673 | color: control.__styleInstance.color |
674 | selectedTextColor: Theme.palette.selected.foregroundText |
675 | selectionColor: Theme.palette.selected.foreground |
676 | @@ -1021,6 +1004,13 @@ |
677 | input: editor |
678 | flickable: flicker |
679 | selectionModeTimeout: control.__styleInstance.selectionModeTimeout |
680 | + /* |
681 | + In x direction we use 2 times the configured spacing, as we have |
682 | + both the overlay and the Flickable aligned with margins. On y |
683 | + direction we only use the simple spacing, the Flickable moves the |
684 | + top downwards. |
685 | + */ |
686 | + frameDistance: Qt.point(2 * internal.spacing, internal.spacing) |
687 | } |
688 | } |
689 | } |
690 | |
691 | === modified file 'modules/Ubuntu/Components/Themes/Ambiance/TextAreaStyle.qml' |
692 | --- modules/Ubuntu/Components/Themes/Ambiance/TextAreaStyle.qml 2014-04-25 12:53:58 +0000 |
693 | +++ modules/Ubuntu/Components/Themes/Ambiance/TextAreaStyle.qml 2014-05-06 12:22:48 +0000 |
694 | @@ -18,6 +18,7 @@ |
695 | import Ubuntu.Components 1.1 |
696 | |
697 | // frame |
698 | +// FIXME: stabilize API |
699 | Item { |
700 | id: visuals |
701 | // style properties |
702 | @@ -36,17 +37,31 @@ |
703 | Spacing between the frame and the text editor area |
704 | */ |
705 | property real frameSpacing: units.gu(1) |
706 | - property real overlaySpacing: units.gu(0.5) |
707 | + property real overlaySpacing: frameSpacing / 2 |
708 | |
709 | /*! |
710 | Property holding the timeout in milliseconds the component enters into selection mode. |
711 | */ |
712 | property int selectionModeTimeout: 300 |
713 | |
714 | + /*! |
715 | + The following properties define the name of the style components declaring |
716 | + the styles for the main and the selection cursors. All cursors must defive |
717 | + from TextCursorStyle. |
718 | + */ |
719 | + property string mainCursorStyle: "TextCursorStyle.qml" |
720 | + property string selectionStartCursorStyle: "TextSelectionStartCursorStyle.qml" |
721 | + property string selectionEndCursorStyle: "TextSelectionEndCursorStyle.qml" |
722 | + |
723 | + // style body |
724 | anchors.fill: parent |
725 | + objectName: "textarea_style" |
726 | |
727 | z: -1 |
728 | |
729 | + /*! |
730 | + Text input background |
731 | + */ |
732 | property Component background: UbuntuShape { |
733 | property bool error: (styledItem.hasOwnProperty("errorHighlight") && styledItem.errorHighlight && !styledItem.acceptableInput) |
734 | onErrorChanged: (error) ? visuals.errorColor : visuals.backgroundColor; |
735 | |
736 | === modified file 'modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml' |
737 | --- modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml 2013-07-01 06:33:24 +0000 |
738 | +++ modules/Ubuntu/Components/Themes/Ambiance/TextCursorStyle.qml 2014-05-06 12:22:48 +0000 |
739 | @@ -16,48 +16,66 @@ |
740 | |
741 | import QtQuick 2.0 |
742 | |
743 | +// FIXME : move the API into Ubuntu.Components.Style |
744 | Item { |
745 | - id: visuals |
746 | - /*! |
747 | - Cursor color |
748 | - */ |
749 | - property color color: Theme.palette.selected.foreground |
750 | - |
751 | - /*! |
752 | - Properties driving cursor blinking. If either of these values are 0, no |
753 | - blinking is provided. |
754 | - */ |
755 | - property bool blinking: true |
756 | - property int blinkTimeoutShown: 800 |
757 | - property int blinkTimeoutHidden: 400 |
758 | - |
759 | - /*! |
760 | - Selection mode pin styles |
761 | - */ |
762 | - property var pinSize: units.gu(1.2) |
763 | - property var pinSensingOffset: units.dp(4) |
764 | - property color pinColor: Theme.palette.selected.foreground |
765 | - |
766 | - anchors.fill: parent |
767 | - Rectangle { |
768 | - id: cursor |
769 | - |
770 | - property bool showCursor: styledItem.visible |
771 | - property bool timerShowCursor: true |
772 | - |
773 | - visible: showCursor && timerShowCursor |
774 | - color: visuals.color |
775 | - anchors.fill: parent |
776 | - |
777 | - Timer { |
778 | - interval: visuals.blinkTimeoutShown |
779 | - running: cursor.showCursor && (visuals.blinkTimeoutShown > 0) && (visuals.blinkTimeoutHidden > 0) && visuals.blinking |
780 | - repeat: true |
781 | - onTriggered: { |
782 | - interval = (interval == visuals.blinkTimeoutShown) ? |
783 | - visuals.blinkTimeoutHidden : visuals.blinkTimeoutShown; |
784 | - cursor.timerShowCursor = !cursor.timerShowCursor; |
785 | + id: cursorStyle |
786 | + /*! |
787 | + Property specifying the visible timeout of the cursor. It is not mandatory |
788 | + for styles to define values for this property if the cursor blinking is not |
789 | + desired. A value of 0 turns off the cursor blinking. |
790 | + */ |
791 | + property int cursorVisibleTimeout: 800 |
792 | + /*! |
793 | + Property specifying the hidden timeout of the cursor. It is not mandatory |
794 | + for styles to define values for this property if the cursor blinking is not |
795 | + desired. A value of 0 turns off the cursor blinking. |
796 | + */ |
797 | + property int cursorHiddenTimeout: 400 |
798 | + |
799 | + /*! |
800 | + Component defining the default cursor visuals. |
801 | + */ |
802 | + property Component cursorDelegate: delegate |
803 | + |
804 | + /*! |
805 | + The item pointing to the cursor handler. Styles should set to null if the |
806 | + cursor does not have handler at all. |
807 | + */ |
808 | + property Item caret: caretItem |
809 | + |
810 | + // style body |
811 | + Component { |
812 | + id: delegate |
813 | + Rectangle { |
814 | + width: units.dp(1) |
815 | + color: Theme.palette.selected.foreground |
816 | + visible: blinkTimer.timerShowCursor |
817 | + Timer { |
818 | + id: blinkTimer |
819 | + interval: cursorStyle.cursorVisibleTimeout |
820 | + running: (cursorStyle.cursorVisibleTimeout > 0) && |
821 | + (cursorStyle.cursorHiddenTimeout > 0) && |
822 | + styledItem.visible |
823 | + repeat: true |
824 | + property bool timerShowCursor: true |
825 | + onTriggered: { |
826 | + interval = (interval == cursorStyle.cursorVisibleTimeout) ? |
827 | + cursorStyle.cursorHiddenTimeout : cursorStyle.cursorVisibleTimeout; |
828 | + timerShowCursor = !timerShowCursor; |
829 | + } |
830 | } |
831 | } |
832 | } |
833 | + |
834 | + // caretItem |
835 | + Image { |
836 | + id: caretItem |
837 | + source: "artwork/teardrop-left.png" |
838 | + anchors { |
839 | + top: parent.bottom |
840 | + horizontalCenter: parent.horizontalCenter |
841 | + topMargin: -units.gu(0.5) |
842 | + horizontalCenterOffset: units.gu(0.7) |
843 | + } |
844 | + } |
845 | } |
846 | |
847 | === modified file 'modules/Ubuntu/Components/Themes/Ambiance/TextFieldStyle.qml' |
848 | --- modules/Ubuntu/Components/Themes/Ambiance/TextFieldStyle.qml 2013-06-27 15:20:12 +0000 |
849 | +++ modules/Ubuntu/Components/Themes/Ambiance/TextFieldStyle.qml 2014-05-06 12:22:48 +0000 |
850 | @@ -17,4 +17,5 @@ |
851 | import QtQuick 2.0 |
852 | |
853 | TextAreaStyle { |
854 | + objectName: "textfield_style" |
855 | } |
856 | |
857 | === added file 'modules/Ubuntu/Components/Themes/Ambiance/TextSelectionEndCursorStyle.qml' |
858 | --- modules/Ubuntu/Components/Themes/Ambiance/TextSelectionEndCursorStyle.qml 1970-01-01 00:00:00 +0000 |
859 | +++ modules/Ubuntu/Components/Themes/Ambiance/TextSelectionEndCursorStyle.qml 2014-05-06 12:22:48 +0000 |
860 | @@ -0,0 +1,66 @@ |
861 | +/* |
862 | + * Copyright 2014 Canonical Ltd. |
863 | + * |
864 | + * This program is free software; you can redistribute it and/or modify |
865 | + * it under the terms of the GNU Lesser General Public License as published by |
866 | + * the Free Software Foundation; version 3. |
867 | + * |
868 | + * This program is distributed in the hope that it will be useful, |
869 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
870 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
871 | + * GNU Lesser General Public License for more details. |
872 | + * |
873 | + * You should have received a copy of the GNU Lesser General Public License |
874 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
875 | + */ |
876 | + |
877 | +import QtQuick 2.0 |
878 | + |
879 | +// FIXME : move the API into Ubuntu.Components.Style |
880 | +Item { |
881 | + id: cursorStyle |
882 | + /*! |
883 | + Property specifying the visible timeout of the cursor. It is not mandatory |
884 | + for styles to define values for this property if the cursor blinking is not |
885 | + desired. A value of 0 turns off the cursor blinking. |
886 | + */ |
887 | + property int cursorVisibleTimeout: 0 |
888 | + /*! |
889 | + Property specifying the hidden timeout of the cursor. It is not mandatory |
890 | + for styles to define values for this property if the cursor blinking is not |
891 | + desired. A value of 0 turns off the cursor blinking. |
892 | + */ |
893 | + property int cursorHiddenTimeout: 0 |
894 | + |
895 | + /*! |
896 | + Component defining the default cursor visuals. |
897 | + */ |
898 | + property Component cursorDelegate: delegate |
899 | + |
900 | + /*! |
901 | + The item pointing to the cursor handler. Styles should set to null if the |
902 | + cursor does not have handler at all. |
903 | + */ |
904 | + property Item caret: caretItem |
905 | + |
906 | + // style body |
907 | + Component { |
908 | + id: delegate |
909 | + Rectangle { |
910 | + width: units.dp(1) |
911 | + color: Theme.palette.selected.foreground |
912 | + } |
913 | + } |
914 | + |
915 | + // caretItem |
916 | + Image { |
917 | + id: caretItem |
918 | + source: "artwork/teardrop-left.png" |
919 | + anchors { |
920 | + top: parent.bottom |
921 | + horizontalCenter: parent.horizontalCenter |
922 | + topMargin: -units.gu(0.5) |
923 | + horizontalCenterOffset: units.gu(0.7) |
924 | + } |
925 | + } |
926 | +} |
927 | |
928 | === added file 'modules/Ubuntu/Components/Themes/Ambiance/TextSelectionStartCursorStyle.qml' |
929 | --- modules/Ubuntu/Components/Themes/Ambiance/TextSelectionStartCursorStyle.qml 1970-01-01 00:00:00 +0000 |
930 | +++ modules/Ubuntu/Components/Themes/Ambiance/TextSelectionStartCursorStyle.qml 2014-05-06 12:22:48 +0000 |
931 | @@ -0,0 +1,66 @@ |
932 | +/* |
933 | + * Copyright 2014 Canonical Ltd. |
934 | + * |
935 | + * This program is free software; you can redistribute it and/or modify |
936 | + * it under the terms of the GNU Lesser General Public License as published by |
937 | + * the Free Software Foundation; version 3. |
938 | + * |
939 | + * This program is distributed in the hope that it will be useful, |
940 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
941 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
942 | + * GNU Lesser General Public License for more details. |
943 | + * |
944 | + * You should have received a copy of the GNU Lesser General Public License |
945 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
946 | + */ |
947 | + |
948 | +import QtQuick 2.0 |
949 | + |
950 | +// FIXME : move the API into Ubuntu.Components.Style |
951 | +Item { |
952 | + id: cursorStyle |
953 | + /*! |
954 | + Property specifying the visible timeout of the cursor. It is not mandatory |
955 | + for styles to define values for this property if the cursor blinking is not |
956 | + desired. A value of 0 turns off the cursor blinking. |
957 | + */ |
958 | + property int cursorVisibleTimeout: 0 |
959 | + /*! |
960 | + Property specifying the hidden timeout of the cursor. It is not mandatory |
961 | + for styles to define values for this property if the cursor blinking is not |
962 | + desired. A value of 0 turns off the cursor blinking. |
963 | + */ |
964 | + property int cursorHiddenTimeout: 0 |
965 | + |
966 | + /*! |
967 | + Component defining the default cursor visuals. |
968 | + */ |
969 | + property Component cursorDelegate: delegate |
970 | + |
971 | + /*! |
972 | + The item pointing to the cursor handler. Styles should set to null if the |
973 | + cursor does not have handler at all. |
974 | + */ |
975 | + property Item caret: caretItem |
976 | + |
977 | + // style body |
978 | + Component { |
979 | + id: delegate |
980 | + Rectangle { |
981 | + width: units.dp(1) |
982 | + color: Theme.palette.selected.foreground |
983 | + } |
984 | + } |
985 | + |
986 | + // caretItem |
987 | + Image { |
988 | + id: caretItem |
989 | + source: "artwork/teardrop-right.png" |
990 | + anchors { |
991 | + top: parent.bottom |
992 | + horizontalCenter: parent.horizontalCenter |
993 | + topMargin: -units.gu(0.5) |
994 | + horizontalCenterOffset: -units.gu(0.7) |
995 | + } |
996 | + } |
997 | +} |
998 | |
999 | === added file 'modules/Ubuntu/Components/Themes/Ambiance/artwork/teardrop-left@20.png' |
1000 | Binary files modules/Ubuntu/Components/Themes/Ambiance/artwork/teardrop-left@20.png 1970-01-01 00:00:00 +0000 and modules/Ubuntu/Components/Themes/Ambiance/artwork/teardrop-left@20.png 2014-05-06 12:22:48 +0000 differ |
1001 | === added file 'modules/Ubuntu/Components/Themes/Ambiance/artwork/teardrop-right@20.png' |
1002 | Binary files modules/Ubuntu/Components/Themes/Ambiance/artwork/teardrop-right@20.png 1970-01-01 00:00:00 +0000 and modules/Ubuntu/Components/Themes/Ambiance/artwork/teardrop-right@20.png 2014-05-06 12:22:48 +0000 differ |
1003 | === modified file 'modules/Ubuntu/Components/Themes/Ambiance/qmldir' |
1004 | --- modules/Ubuntu/Components/Themes/Ambiance/qmldir 2014-04-28 14:04:29 +0000 |
1005 | +++ modules/Ubuntu/Components/Themes/Ambiance/qmldir 2014-05-06 12:22:48 +0000 |
1006 | @@ -46,3 +46,6 @@ |
1007 | |
1008 | #version 1.1 |
1009 | ComboButtonStyle 1.1 ComboButtonStyle.qml |
1010 | + |
1011 | +internal TextSelectionStartCursorStyle TextSelectionStartCursorStyle.qml |
1012 | +internal TextSelectionEndCursorStyle TextSelectionEndCursorStyle.qml |
1013 | |
1014 | === modified file 'modules/Ubuntu/Test/UbuntuTestCase.qml' |
1015 | --- modules/Ubuntu/Test/UbuntuTestCase.qml 2014-04-28 06:06:32 +0000 |
1016 | +++ modules/Ubuntu/Test/UbuntuTestCase.qml 2014-05-06 12:22:48 +0000 |
1017 | @@ -66,6 +66,28 @@ |
1018 | return null; |
1019 | } |
1020 | |
1021 | + /*! |
1022 | + Finds a visible child of an \a item having a given \a property with a given |
1023 | + \a value. |
1024 | + */ |
1025 | + function findChildWithProperty(item, property, value) { |
1026 | + var childs = new Array(0); |
1027 | + childs.push(item) |
1028 | + while (childs.length > 0) { |
1029 | + var child = childs[0]; |
1030 | + if (child.hasOwnProperty(property) && (child[property] === value)) { |
1031 | + return child; |
1032 | + } |
1033 | + for (var i in childs[0].children) { |
1034 | + childs.push(childs[0].children[i]) |
1035 | + } |
1036 | + childs.splice(0, 1); |
1037 | + } |
1038 | + return null; |
1039 | + } |
1040 | + |
1041 | + |
1042 | + |
1043 | /*! |
1044 | Move Mouse from x,y to distance of dx, dy divided to steps with a stepdelay (ms). |
1045 | */ |
1046 | @@ -152,8 +174,9 @@ |
1047 | */ |
1048 | function mouseLongPress(item, x, y, button, modifiers, delay) { |
1049 | mousePress(item, x, y, button, modifiers, delay); |
1050 | - // the delay is taken from QQuickMouseArea |
1051 | - wait(800); |
1052 | + // the delay is taken from QQuickMouseArea, add few miliseconds to it to make sure we have |
1053 | + // the long press triggered |
1054 | + wait(900); |
1055 | } |
1056 | |
1057 | /*! |
1058 | |
1059 | === modified file 'tests/resources/inputs/TextInputs.qml' |
1060 | --- tests/resources/inputs/TextInputs.qml 2014-04-07 10:37:36 +0000 |
1061 | +++ tests/resources/inputs/TextInputs.qml 2014-05-06 12:22:48 +0000 |
1062 | @@ -74,6 +74,10 @@ |
1063 | autoSize: true |
1064 | maximumLineCount: 0 |
1065 | text: "Lorem Ipsum is simply dummy text\nof the printing and typesetting\nindustry.\n" |
1066 | + cursorDelegate: Rectangle { |
1067 | + width: units.dp(4) |
1068 | + color: "blue" |
1069 | + } |
1070 | } |
1071 | TextArea { |
1072 | autoSize: true |
1073 | |
1074 | === modified file 'tests/unit_x11/tst_components/tst_textarea.qml' |
1075 | --- tests/unit_x11/tst_components/tst_textarea.qml 2014-04-28 06:06:32 +0000 |
1076 | +++ tests/unit_x11/tst_components/tst_textarea.qml 2014-05-06 12:22:48 +0000 |
1077 | @@ -138,7 +138,7 @@ |
1078 | } |
1079 | |
1080 | function test_0_cursorDelegate() { |
1081 | - compare((textArea.cursorDelegate!=null),true,"TextArea.cursorDelegate is not null") |
1082 | + verify(textArea.cursorDelegate === null, "TextArea.cursorDelegate is not null") |
1083 | } |
1084 | |
1085 | function test_0_cursorPosition() { |
1086 | @@ -242,15 +242,16 @@ |
1087 | compare(textArea.highlighted, textArea.focus, "highlighted is the same as focused"); |
1088 | } |
1089 | |
1090 | - function test_contentHeight() { |
1091 | - compare(textArea.contentHeight>0,true,"contentHeight over 0 units on default") |
1092 | + function test_0_contentHeight() { |
1093 | + verify(textArea.contentHeight > 0, "contentHeight over 0 units on default") |
1094 | var newValue = 200; |
1095 | textArea.contentHeight = newValue; |
1096 | compare(textArea.contentHeight,newValue,"set/get"); |
1097 | } |
1098 | |
1099 | - function test_contentWidth() { |
1100 | - compare(textArea.contentWidth,units.gu(30),"contentWidth is 30 units on default") |
1101 | + function test_0_contentWidth() { |
1102 | + var style = findChild(textArea, "textarea_style"); |
1103 | + compare(textArea.contentWidth, units.gu(30) - 2 * style.frameSpacing, "contentWidth is the implicitWidth - 2 times the frame size on default") |
1104 | var newValue = 200; |
1105 | textArea.contentWidth = newValue; |
1106 | compare(textArea.contentWidth,newValue,"set/get"); |
1107 | |
1108 | === modified file 'tests/unit_x11/tst_components/tst_textfield.qml' |
1109 | --- tests/unit_x11/tst_components/tst_textfield.qml 2014-04-28 06:06:32 +0000 |
1110 | +++ tests/unit_x11/tst_components/tst_textfield.qml 2014-05-06 12:22:48 +0000 |
1111 | @@ -156,7 +156,7 @@ |
1112 | } |
1113 | |
1114 | function test_0_cursorDelegate() { |
1115 | - verify(textField.cursorDelegate, "cursorDelegate set by default") |
1116 | + verify(textField.cursorDelegate === null, "cursorDelegate not set by default") |
1117 | } |
1118 | |
1119 | function test_0_cursorPosition() { |
1120 | |
1121 | === added file 'tests/unit_x11/tst_components/tst_textinput_common.qml' |
1122 | --- tests/unit_x11/tst_components/tst_textinput_common.qml 1970-01-01 00:00:00 +0000 |
1123 | +++ tests/unit_x11/tst_components/tst_textinput_common.qml 2014-05-06 12:22:48 +0000 |
1124 | @@ -0,0 +1,198 @@ |
1125 | +/* |
1126 | + * Copyright 2014 Canonical Ltd. |
1127 | + * |
1128 | + * This program is free software; you can redistribute it and/or modify |
1129 | + * it under the terms of the GNU Lesser General Public License as published by |
1130 | + * the Free Software Foundation; version 3. |
1131 | + * |
1132 | + * This program is distributed in the hope that it will be useful, |
1133 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1134 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1135 | + * GNU Lesser General Public License for more details. |
1136 | + * |
1137 | + * You should have received a copy of the GNU Lesser General Public License |
1138 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1139 | + */ |
1140 | + |
1141 | +import QtQuick 2.0 |
1142 | +import QtTest 1.0 |
1143 | +import Ubuntu.Test 1.0 |
1144 | +import Ubuntu.Components 1.1 |
1145 | + |
1146 | +Item { |
1147 | + id: testMain |
1148 | + width: units.gu(40) |
1149 | + height: units.gu(71) |
1150 | + |
1151 | + Column { |
1152 | + spacing: units.gu(1) |
1153 | + TextField { |
1154 | + id: textField |
1155 | + } |
1156 | + TextArea { |
1157 | + id: textArea |
1158 | + } |
1159 | + } |
1160 | + |
1161 | + SignalSpy { |
1162 | + id: cursorPositionSpy |
1163 | + signalName: "onCursorPositionChanged" |
1164 | + } |
1165 | + SignalSpy { |
1166 | + id: selectionStartSpy |
1167 | + signalName: "onSelectionStartChanged" |
1168 | + } |
1169 | + SignalSpy { |
1170 | + id: selectionEndSpy |
1171 | + signalName: "onSelectionEndChanged" |
1172 | + } |
1173 | + SignalSpy { |
1174 | + id: selectedTextSpy |
1175 | + signalName: "onSelectedTextChanged" |
1176 | + } |
1177 | + SignalSpy { |
1178 | + id: popupSpy |
1179 | + signalName: "pressAndHold" |
1180 | + } |
1181 | + SignalSpy { |
1182 | + id: movementXSpy |
1183 | + signalName: "onContentXChanged" |
1184 | + } |
1185 | + SignalSpy { |
1186 | + id: movementYSpy |
1187 | + signalName: "contentYChanged" |
1188 | + } |
1189 | + |
1190 | + UbuntuTestCase { |
1191 | + name: "TextInputCaretTest" |
1192 | + when: windowShown |
1193 | + |
1194 | + function init() { |
1195 | + textField.text = "This is a single line text input called TextField."; |
1196 | + textArea.text = "This is a multiline text input component called TextArea. It supports fix size as well as auto-expanding behavior."; |
1197 | + textField.cursorPosition = 0; |
1198 | + textArea.cursorPosition = 0; |
1199 | + waitForRendering(textField, 500); |
1200 | + waitForRendering(textArea, 500); |
1201 | + } |
1202 | + |
1203 | + function cleanup() { |
1204 | + textField.focus = false; |
1205 | + textArea.focus = false; |
1206 | + cursorPositionSpy.clear(); |
1207 | + selectionStartSpy.clear(); |
1208 | + selectionEndSpy.clear(); |
1209 | + selectedTextSpy.clear(); |
1210 | + popupSpy.clear(); |
1211 | + movementXSpy.clear(); |
1212 | + movementYSpy.clear(); |
1213 | + } |
1214 | + |
1215 | + function test_textfield_grab_caret_data() { |
1216 | + return [ |
1217 | + {input: textField}, |
1218 | + {input: textArea}, |
1219 | + ]; |
1220 | + } |
1221 | + |
1222 | + function test_textfield_grab_caret(data) { |
1223 | + data.input.focus = true; |
1224 | + |
1225 | + var draggedItem = findChild(data.input, "cursorPosition_draggeditem"); |
1226 | + var drag_activator = findChild(data.input, "cursorPosition_activator"); |
1227 | + var dragger = findChild(data.input, "cursorPosition_dragger"); |
1228 | + |
1229 | + // drag the cursor, observe cursorPositionChanged |
1230 | + cursorPositionSpy.target = data.input; |
1231 | + var x = drag_activator.width / 2; |
1232 | + var y = drag_activator.height / 2; |
1233 | + mousePress(drag_activator, x, y); |
1234 | + compare(draggedItem.state, "dragging", "the caret hasn't been activated"); |
1235 | + // drag the mouse |
1236 | + mouseMoveSlowly(dragger, x, y, 100, 0, 10, 0); |
1237 | + mouseRelease(dragger, x + 100, y); |
1238 | + waitForRendering(data.input, 1000); |
1239 | + compare(draggedItem.state, "", "the caret hasn't been deactivated"); |
1240 | + cursorPositionSpy.wait(); |
1241 | + } |
1242 | + |
1243 | + function test_textfield_grab_selection_cursors_data() { |
1244 | + return [ |
1245 | + {input: textField, cursorSpy: selectionStartSpy, dragPrefix: "selectionStart", startDx: 0, startDy: 0, dragDx: 100, dragDy: 0, movingSpy: null}, |
1246 | + {input: textArea, cursorSpy: selectionStartSpy, dragPrefix: "selectionStart", startDx: 0, startDy: 0, dragDx: 100, dragDy: 0, movingSpy: null}, |
1247 | + {input: textField, cursorSpy: selectionEndSpy, dragPrefix: "selectionEnd", startDx: 0, startDy: 0, dragDx: 200, dragDy: 100, movingSpy: movementXSpy, flicker: "textfield_scroller"}, |
1248 | + {input: textArea, cursorSpy: selectionEndSpy, dragPrefix: "selectionEnd", startDx: 0, startDy: 0, dragDx: 200, dragDy: 100, movingSpy: movementYSpy, flicker: "textarea_scroller"}, |
1249 | + ]; |
1250 | + } |
1251 | + function test_textfield_grab_selection_cursors(data) { |
1252 | + if (data.movingSpy) { |
1253 | + var flicker = findChild(data.input, data.flicker) |
1254 | + data.movingSpy.target = flicker; |
1255 | + } |
1256 | + data.input.focus = true; |
1257 | + // make sure the text is long enough |
1258 | + data.input.text += data.input.text; |
1259 | + |
1260 | + // select text in front |
1261 | + data.input.select(0, 10); |
1262 | + var draggedItem = findChild(data.input, data.dragPrefix + "_draggeditem"); |
1263 | + var drag_activator = findChild(data.input, data.dragPrefix + "_activator"); |
1264 | + var dragger = findChild(data.input, data.dragPrefix + "_dragger"); |
1265 | + data.cursorSpy.target = data.input; |
1266 | + |
1267 | + // grab the selection and drag it |
1268 | + var x = drag_activator.width / 2 + data.startDx; |
1269 | + var y = drag_activator.height / 2 + data.startDy; |
1270 | + selectedTextSpy.target = data.input; |
1271 | + mousePress(drag_activator, x, y); |
1272 | + compare(draggedItem.state, "dragging", "the caret hasn't been activated"); |
1273 | + // drag the mouse |
1274 | + mouseMoveSlowly(dragger, x, y, data.dragDx, data.dragDy, 15, 100); |
1275 | + mouseRelease(dragger, x + data.dragDx, y + data.dragDy); |
1276 | + waitForRendering(data.input, 500); |
1277 | + compare(draggedItem.state, "", "the caret hasn't been deactivated"); |
1278 | + data.cursorSpy.wait(); |
1279 | + selectedTextSpy.wait(); |
1280 | + if (data.movingSpy) { |
1281 | + data.movingSpy.wait(); |
1282 | + } |
1283 | + } |
1284 | + |
1285 | + function test_clear_text_using_popover_data() { |
1286 | + return [ |
1287 | + {input: textField}, |
1288 | + {input: textArea}, |
1289 | + ]; |
1290 | + } |
1291 | + |
1292 | + function test_clear_text_using_popover(data) { |
1293 | + var handler = findChild(data.input, "input_handler"); |
1294 | + popupSpy.target = handler; |
1295 | + data.input.focus = true; |
1296 | + |
1297 | + // invoke popover |
1298 | + var x = data.input.width / 2; |
1299 | + var y = data.input.height / 2; |
1300 | + mouseLongPress(data.input, x, y); |
1301 | + popupSpy.wait(); |
1302 | + mouseRelease(data.input, x, y); |
1303 | + var popover = findChild(testMain, "text_input_popover"); |
1304 | + verify(popover, "Cannot retrieve default TextInputPopover"); |
1305 | + waitForRendering(popover); |
1306 | + // select all |
1307 | + var selectAll = findChildWithProperty(popover, "text", "Select All"); |
1308 | + verify(selectAll, "Select All item not found"); |
1309 | + mouseClick(selectAll, selectAll.width / 2, selectAll.height / 2); |
1310 | + waitForRendering(data.input, 1000); |
1311 | + compare(data.input.text, data.input.selectedText, "Not all the text is selected"); |
1312 | + // delete with key press |
1313 | + keyClick(Qt.Key_Backspace); |
1314 | + waitForRendering(data.input, 1000); |
1315 | + compare(data.input.text, "", "The text has not been deleted"); |
1316 | + |
1317 | + // dismiss popover |
1318 | + mouseClick(testMain, testMain.width / 2, testMain.height / 2); |
1319 | + wait(200); |
1320 | + } |
1321 | + } |
1322 | +} |
The carrots actually do a nice job on the desktop enabling me to extend selections with just a mouse - ordinarily it would require ⇧Arrow.
A single carrot is visible when there's no selection but can't be dragged.
If the selection is at the very start of the text field the first carrot is clipped.
505 - Ubuntu. Mouse.forwardTo : [inputHandler] Mouse.forwardTo : [inputHandler]
506 +// Ubuntu.
Accident?
526 +// FIXME: stabilize API
What does this mean? Could you make the comment more verbose or mention a bug?