improved desktop UI
[mardrone] / mardrone / imports / com / nokia / meego / TextField.qml
1 /****************************************************************************
2 **
3 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the Qt Components project.
8 **
9 ** $QT_BEGIN_LICENSE:BSD$
10 ** You may use this file under the terms of the BSD license as follows:
11 **
12 ** "Redistribution and use in source and binary forms, with or without
13 ** modification, are permitted provided that the following conditions are
14 ** met:
15 **   * Redistributions of source code must retain the above copyright
16 **     notice, this list of conditions and the following disclaimer.
17 **   * Redistributions in binary form must reproduce the above copyright
18 **     notice, this list of conditions and the following disclaimer in
19 **     the documentation and/or other materials provided with the
20 **     distribution.
21 **   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
22 **     the names of its contributors may be used to endorse or promote
23 **     products derived from this software without specific prior written
24 **     permission.
25 **
26 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
37 ** $QT_END_LICENSE$
38 **
39 ****************************************************************************/
40
41 import QtQuick 1.1
42 import "." 1.0
43 import "UIConstants.js" as UI
44 import "EditBubble.js" as Popup
45 import "TextAreaHelper.js" as TextAreaHelper
46 import "Magnifier.js" as MagnifierPopup
47 FocusScope {
48     id: root
49
50     // Common public API
51     property alias text: textInput.text
52     property alias placeholderText: prompt.text
53
54     property alias inputMethodHints: textInput.inputMethodHints
55     property alias font: textInput.font
56     property alias cursorPosition: textInput.cursorPosition
57     property alias maximumLength: textInput.maximumLength
58     property alias readOnly: textInput.readOnly
59     property alias acceptableInput: textInput.acceptableInput
60     property alias inputMask: textInput.inputMask
61     property alias validator: textInput.validator
62
63     property alias selectedText: textInput.selectedText
64     property alias selectionStart: textInput.selectionStart
65     property alias selectionEnd: textInput.selectionEnd
66
67     property alias echoMode: textInput.echoMode // ### TODO: declare own enum { Normal, Password }
68
69     property bool errorHighlight: !acceptableInput
70     // Property enableSoftwareInputPanel is DEPRECATED
71     property alias enableSoftwareInputPanel: textInput.activeFocusOnPress
72
73     property Item platformSipAttributes
74
75     property bool platformEnableEditBubble: true
76
77     property Item platformStyle: TextFieldStyle {}
78
79     property alias style: root.platformStyle
80
81     property Component customSoftwareInputPanel
82
83     property Component platformCustomSoftwareInputPanel
84
85     property alias platformPreedit: inputMethodObserver.preedit
86
87     signal accepted
88
89     onPlatformSipAttributesChanged: {
90         platformSipAttributes.registerInputElement(textInput)
91     }
92
93     onCustomSoftwareInputPanelChanged: {
94         console.log("TextField's property customSoftwareInputPanel is deprecated. Use property platformCustomSoftwareInputPanel instead.")
95         platformCustomSoftwareInputPanel = customSoftwareInputPanel
96     }
97
98     onPlatformCustomSoftwareInputPanelChanged: {
99         textInput.activeFocusOnPress = platformCustomSoftwareInputPanel == null
100     }
101
102
103
104     function copy() {
105         textInput.copy()
106     }
107
108     Connections {
109         target: platformWindow
110
111         onActiveChanged: {
112             if(platformWindow.active) {
113                 if (!readOnly) {
114                     if (activeFocus) {
115                         if (platformCustomSoftwareInputPanel != null) {
116                             platformOpenSoftwareInputPanel();
117                         } else {
118                             inputContext.simulateSipOpen();
119                         }
120                         repositionTimer.running = true;
121                     }
122                 }
123             } else {
124                 if (activeFocus) {
125                     platformCloseSoftwareInputPanel();
126                     Popup.close(textInput);
127                 }
128             }
129         }
130
131         onAnimatingChanged: {
132             if (!platformWindow.animating && root.activeFocus) {
133                 TextAreaHelper.repositionFlickable(contentMovingAnimation);
134             }
135         }
136     }
137
138
139     function paste() {
140         textInput.paste()
141     }
142
143     function cut() {
144         textInput.cut()
145     }
146
147     function select(start, end) {
148         textInput.select(start, end)
149     }
150
151     function selectAll() {
152         textInput.selectAll()
153     }
154
155     function selectWord() {
156         textInput.selectWord()
157     }
158
159     function positionAt(x) {
160         var p = mapToItem(textInput, x, 0);
161         return textInput.positionAt(p.x)
162     }
163
164     function positionToRectangle(pos) {
165         var rect = textInput.positionToRectangle(pos)
166         rect.x = mapFromItem(textInput, rect.x, 0).x
167         return rect;
168     }
169
170     // ensure propagation of forceActiveFocus
171     function forceActiveFocus() {
172         textInput.forceActiveFocus()
173     }
174
175     function closeSoftwareInputPanel() {
176         console.log("TextField's function closeSoftwareInputPanel is deprecated. Use function platformCloseSoftwareInputPanel instead.")
177         platformCloseSoftwareInputPanel()
178     }
179
180     function platformCloseSoftwareInputPanel() {
181         inputContext.simulateSipClose();
182         if (inputContext.customSoftwareInputPanelVisible) {
183             inputContext.customSoftwareInputPanelVisible = false
184             inputContext.customSoftwareInputPanelComponent = null
185             inputContext.customSoftwareInputPanelTextField = null
186         } else {
187             textInput.closeSoftwareInputPanel();
188         }
189     }
190
191     function openSoftwareInputPanel() {
192         console.log("TextField's function openSoftwareInputPanel is deprecated. Use function platformOpenSoftwareInputPanel instead.")
193         platformOpenSoftwareInputPanel()
194     }
195
196     function platformOpenSoftwareInputPanel() {
197         inputContext.simulateSipOpen();
198         if (platformCustomSoftwareInputPanel != null && !inputContext.customSoftwareInputPanelVisible) {
199             inputContext.customSoftwareInputPanelTextField = root
200             inputContext.customSoftwareInputPanelComponent = platformCustomSoftwareInputPanel
201             inputContext.customSoftwareInputPanelVisible = true
202         } else {
203             textInput.openSoftwareInputPanel();
204         }
205     }
206
207     // private
208     property bool __expanding: true // Layout hint used but ToolBarLayout
209     property int __preeditDisabledMask: Qt.ImhHiddenText|
210                                         Qt.ImhNoPredictiveText|
211                                         Qt.ImhDigitsOnly|
212                                         Qt.ImhFormattedNumbersOnly|
213                                         Qt.ImhDialableCharactersOnly|
214                                         Qt.ImhEmailCharactersOnly|
215                                         Qt.ImhUrlCharactersOnly 
216
217     implicitWidth: platformStyle.defaultWidth
218     implicitHeight: UI.FIELD_DEFAULT_HEIGHT
219
220     onActiveFocusChanged: {
221         if (!readOnly) {
222             if (activeFocus) {
223                 if (platformCustomSoftwareInputPanel != null) {
224                     platformOpenSoftwareInputPanel();
225                 } else {
226                     inputContext.simulateSipOpen();
227                 }
228
229                 repositionTimer.running = true;
230             } else {                
231                 platformCloseSoftwareInputPanel();
232                 Popup.close(textInput);
233             }
234         }
235     }
236
237
238     BorderImage {
239         id: background
240                 source: errorHighlight?
241                     platformStyle.backgroundError:
242                 readOnly?
243                     platformStyle.backgroundDisabled:
244                 textInput.activeFocus? 
245             platformStyle.backgroundSelected:
246                     platformStyle.background
247
248         anchors.fill: parent
249         border.left: root.platformStyle.backgroundCornerMargin; border.top: root.platformStyle.backgroundCornerMargin
250         border.right: root.platformStyle.backgroundCornerMargin; border.bottom: root.platformStyle.backgroundCornerMargin
251     }
252
253     Text {
254         id: prompt
255
256         anchors {verticalCenter: parent.verticalCenter; left: parent.left; right: parent.right}
257         anchors.leftMargin: root.platformStyle.paddingLeft
258         anchors.rightMargin: root.platformStyle.paddingRight
259         anchors.verticalCenterOffset: root.platformStyle.baselineOffset
260
261         font: root.platformStyle.textFont
262         color: root.platformStyle.promptTextColor
263         elide: Text.ElideRight
264
265         // opacity for default state
266         opacity: 0.0
267
268         states: [
269             State {
270                 name: "unfocused"
271                 // memory allocation optimization: cursorPosition is checked to minimize displayText evaluations
272                 when: !root.activeFocus && textInput.cursorPosition == 0 && !textInput.text && prompt.text && !textInput.inputMethodComposing
273                 PropertyChanges { target: prompt; opacity: 1.0; }
274             },
275             State {
276                 name: "focused"
277                 // memory allocation optimization: cursorPosition is checked to minimize displayText evaluations
278                 when: root.activeFocus && textInput.cursorPosition == 0 && !textInput.text && prompt.text && !textInput.inputMethodComposing
279                 PropertyChanges { target: prompt; opacity: 0.6; }
280             }
281         ]
282
283         transitions: [
284             Transition {
285                 from: "unfocused"; to: "focused";
286                 reversible: true
287                 SequentialAnimation {
288                     PauseAnimation { duration: 60 }
289                     NumberAnimation { target: prompt; properties: "opacity"; duration: 150  }
290                 }
291             },
292             Transition {
293                 from: "focused"; to: "";
294                 reversible: true
295                 SequentialAnimation {
296                     PauseAnimation { duration:  60 }
297                     NumberAnimation { target: prompt; properties: "opacity"; duration: 100 }
298                 }
299             }
300         ]
301     }
302
303     MouseArea {
304         enabled: !textInput.activeFocus
305         z: enabled?1:0
306         anchors.fill: parent
307         anchors.margins: UI.TOUCH_EXPANSION_MARGIN
308         onClicked: {
309             if (!textInput.activeFocus) {
310                 textInput.forceActiveFocus();
311
312                 // activate to preedit and/or move the cursor
313                 var preeditDisabled = root.inputMethodHints &
314                                       root.__preeditDisabledMask                         
315                 var injectionSucceeded = false;
316                 var newCursorPosition = textInput.positionAt(mapToItem(textInput, mouseX, mouseY).x,TextInput.CursorOnCharacter);
317                 if (!preeditDisabled
318                         && !TextAreaHelper.atSpace(newCursorPosition)
319                         && newCursorPosition != textInput.text.length
320                         && !(newCursorPosition == 0 || TextAreaHelper.atSpace(newCursorPosition - 1))) {
321                     injectionSucceeded = TextAreaHelper.injectWordToPreedit(newCursorPosition);
322                 }
323                 if (!injectionSucceeded) {
324                     textInput.cursorPosition=newCursorPosition;
325                 }
326             }
327         }
328     }
329
330     TextInput {
331         id: textInput
332
333         property alias preedit: inputMethodObserver.preedit
334         property alias preeditCursorPosition: inputMethodObserver.preeditCursorPosition
335
336         anchors {verticalCenter: parent.verticalCenter; left: parent.left; right: parent.right}
337         anchors.leftMargin: root.platformStyle.paddingLeft
338         anchors.rightMargin: root.platformStyle.paddingRight
339         anchors.verticalCenterOffset: root.platformStyle.baselineOffset
340
341         passwordCharacter: "\u2022"
342         font: root.platformStyle.textFont
343         color: root.platformStyle.textColor
344         selectByMouse: false
345         selectedTextColor: root.platformStyle.selectedTextColor
346         selectionColor: root.platformStyle.selectionColor
347         mouseSelectionMode: TextInput.SelectWords
348         focus: true
349
350         onAccepted: { root.accepted() } 
351
352         Component.onDestruction: {
353             Popup.close(textInput);
354         }
355
356         Connections {
357             target: TextAreaHelper.findFlickable(root.parent)
358
359             onContentYChanged: if (root.activeFocus) TextAreaHelper.filteredInputContextUpdate();
360             onContentXChanged: if (root.activeFocus) TextAreaHelper.filteredInputContextUpdate();
361             onMovementEnded: inputContext.update();
362         }
363
364         Connections {
365             target: inputContext
366
367             onSoftwareInputPanelRectChanged: {
368                 if (activeFocus) {
369                     repositionTimer.running = true
370                 }
371             }
372         }
373
374         onTextChanged: {            
375             if(root.activeFocus) {
376                 TextAreaHelper.repositionFlickable(contentMovingAnimation)
377             }
378
379             if (Popup.isOpened(textInput) && !Popup.isChangingInput())
380                 Popup.close(textInput);
381         }
382
383         onCursorPositionChanged: {
384             if (MagnifierPopup.isOpened() &&
385                 Popup.isOpened()) {
386                 Popup.close(textInput);
387             } else if ((!mouseFilter.attemptToActivate ||
388                 textInput.cursorPosition == textInput.text.length) &&
389                 Popup.isOpened(textInput) &&
390                 !Popup.isChangingInput()) {
391                     Popup.close(textInput);
392                     Popup.open(textInput,
393                         textInput.positionToRectangle(textInput.cursorPosition));
394             }
395         }
396
397         onSelectedTextChanged: {
398             if (Popup.isOpened(textInput) && !Popup.isChangingInput()) {
399                 Popup.close(textInput);
400             }
401         }
402
403         InputMethodObserver {
404             id: inputMethodObserver
405
406             onPreeditChanged: {                
407                 if(root.activeFocus) {
408                     TextAreaHelper.repositionFlickable(contentMovingAnimation)
409                 }
410
411                 if (Popup.isOpened(textInput) && !Popup.isChangingInput()) {
412                     Popup.close(textInput);
413                 }
414             }
415         }
416
417         Timer {
418             id: repositionTimer
419             interval: 350
420             onTriggered: {
421                 TextAreaHelper.repositionFlickable(contentMovingAnimation)
422             }
423         }
424
425         PropertyAnimation {
426             id: contentMovingAnimation
427             property: "contentY"
428             duration: 200
429             easing.type: Easing.InOutCubic
430         }
431
432         MouseFilter {
433             id: mouseFilter
434             anchors.fill: parent
435             anchors.leftMargin:  UI.TOUCH_EXPANSION_MARGIN - root.platformStyle.paddingLeft
436             anchors.rightMargin:  UI.TOUCH_EXPANSION_MARGIN - root.platformStyle.paddingRight
437             anchors.topMargin: UI.TOUCH_EXPANSION_MARGIN - ((root.height - parent.height) / 2)
438             anchors.bottomMargin:  UI.TOUCH_EXPANSION_MARGIN - ((root.height - parent.height) / 2)
439
440             property bool attemptToActivate: false
441             property bool pressOnPreedit: false
442             property int oldCursorPosition: 0
443
444             property variant editBubblePosition: Qt.point(0,0)
445
446             onPressed: {
447                 var mousePosition = textInput.positionAt(mouse.x,TextInput.CursorOnCharacter);
448                 pressOnPreedit = textInput.cursorPosition==mousePosition
449                 oldCursorPosition = textInput.cursorPosition;
450                 var preeditDisabled = root.inputMethodHints &
451                                       root.__preeditDisabledMask
452
453                 attemptToActivate = !pressOnPreedit && !root.readOnly && !preeditDisabled && root.activeFocus &&
454                                     !(mousePosition == 0 || TextAreaHelper.atSpace(mousePosition - 1) || TextAreaHelper.atSpace(mousePosition));
455                 mouse.filtered = true;
456             }
457
458             onDelayedPressSent: {
459                 if (textInput.preedit) {
460                     textInput.cursorPosition = oldCursorPosition;
461                 }
462             }
463
464             onHorizontalDrag: {
465                 // possible pre-edit word have to be commited before selection
466                 if (root.activeFocus || root.readOnly) {
467                     inputContext.reset()                    
468                     parent.selectByMouse = true
469                     attemptToActivate = false
470                 }
471             }
472
473             onPressAndHold:{
474                 // possible pre-edit word have to be commited before showing the magnifier
475                 if ((root.text != "" || inputMethodObserver.preedit != "") && root.activeFocus) {
476                     inputContext.reset()
477                     attemptToActivate = false
478                     MagnifierPopup.open(root);
479                     var magnifier = MagnifierPopup.popup;
480                     var cursorPos = textInput.positionToRectangle(0);
481                     var mappedPosMf = mapFromItem(parent,mouse.x,cursorPos.y+cursorPos.height);
482                     magnifier.xCenter = mapToItem(magnifier.sourceItem,mappedPosMf.x,0).x;
483                     var mappedPos =  mapToItem(magnifier.parent, mappedPosMf.x - magnifier.width / 2,
484                                                textInput.y - 120 - UI.MARGIN_XLARGE - (height / 2));
485                     var yAdjustment = -mapFromItem(magnifier.__rootElement(), 0, 0).y < magnifier.height / 2.5 ? magnifier.height / 2.5 + mapFromItem(magnifier.__rootElement(), 0,0).y : 0
486                     magnifier.x = mappedPos.x;
487                     magnifier.y = mappedPos.y + yAdjustment;
488                     magnifier.yCenter = mapToItem(magnifier.sourceItem,0,mappedPosMf.y).y;
489                     parent.cursorPosition = textInput.positionAt(mouse.x)                    
490                 }
491             }
492
493             onReleased: {
494                 if (MagnifierPopup.isOpened()) {
495                     MagnifierPopup.close();
496                 }
497
498                 if (attemptToActivate)
499                     inputContext.reset();
500
501                 var newCursorPosition = textInput.positionAt(mouse.x,TextInput.CursorOnCharacter); 
502                 editBubblePosition = textInput.positionToRectangle(newCursorPosition);
503
504                 if (attemptToActivate) {
505                     var beforeText = textInput.text;
506
507                     textInput.cursorPosition = newCursorPosition;
508                     var injectionSucceeded = false;
509
510                     if (!TextAreaHelper.atSpace(newCursorPosition)                             
511                              && newCursorPosition != textInput.text.length) {
512                         injectionSucceeded = TextAreaHelper.injectWordToPreedit(newCursorPosition);
513                     }
514                     if (injectionSucceeded) {
515                         mouse.filtered=true;
516                         if (textInput.preedit.length >=1 && textInput.preedit.length <= 4)
517                             editBubblePosition = textInput.positionToRectangle(textInput.cursorPosition+1)
518                     } else {
519                         textInput.text=beforeText;
520                         textInput.cursorPosition=newCursorPosition;
521                     }
522                 } else if (!parent.selectByMouse) {
523                     if (!pressOnPreedit) inputContext.reset();
524                     textInput.cursorPosition = textInput.positionAt(mouse.x,TextInput.CursorOnCharacter);
525                 }
526                 parent.selectByMouse = false;
527             }
528
529             onFinished: {
530                 if (root.activeFocus && platformEnableEditBubble) {
531                     if (textInput.preedit.length == 0) 
532                         editBubblePosition = textInput.positionToRectangle(textInput.cursorPosition);
533                     Popup.open(textInput,editBubblePosition);
534
535                 }
536                 attemptToActivate = false
537             }
538
539             onMousePositionChanged: {
540                 if (MagnifierPopup.isOpened() && !parent.selectByMouse) {
541                     textInput.cursorPosition = textInput.positionAt(mouse.x)
542                     var magnifier = MagnifierPopup.popup;
543                     var mappedPosMf = mapFromItem(parent,mouse.x,0);
544                     var mappedPos =  mapToItem(magnifier.parent,mappedPosMf.x - magnifier.width / 2.0, 0);
545                     magnifier.xCenter = mapToItem(magnifier.sourceItem,mappedPosMf.x,0).x;
546                     magnifier.x = mappedPos.x;
547                 }
548             }
549
550             onDoubleClicked: {
551                 // possible pre-edit word have to be commited before selection
552                 inputContext.reset()
553                 parent.selectByMouse = true
554                 attemptToActivate = false
555             }
556         }
557     }
558
559     InverseMouseArea {
560         anchors.fill: parent
561         anchors.margins: UI.TOUCH_EXPANSION_MARGIN
562         enabled: textInput.activeFocus
563         onClickedOutside: {
564             if (Popup.isOpened(textInput) && ((mouseX > Popup.geometry().left && mouseX < Popup.geometry().right) &&
565                                            (mouseY > Popup.geometry().top && mouseY < Popup.geometry().bottom))) {
566                 return;
567             }
568             root.parent.focus = true;
569         }
570     }
571 }