improved desktop UI
[mardrone] / mardrone / imports / com / nokia / meego / TextArea.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 Qt.labs.components 1.1
44 import "UIConstants.js" as UI
45 import "EditBubble.js" as Popup
46 import "TextAreaHelper.js" as TextAreaHelper
47 import "Magnifier.js" as MagnifierPopup
48
49 FocusScope {
50     id: root
51
52     // Common public API
53     property alias text: textEdit.text
54     property alias placeholderText: prompt.text
55
56     property alias font: textEdit.font
57     property alias cursorPosition: textEdit.cursorPosition
58     property alias readOnly: textEdit.readOnly
59
60     property alias horizontalAlignment: textEdit.horizontalAlignment
61     property alias verticalAlignment: textEdit.verticalAlignment
62
63     property alias selectedText: textEdit.selectedText
64     property alias selectionStart: textEdit.selectionStart
65     property alias selectionEnd: textEdit.selectionEnd
66
67     property alias wrapMode: textEdit.wrapMode
68     property alias textFormat: textEdit.textFormat
69     // Property enableSoftwareInputPanel is DEPRECATED
70     property alias enableSoftwareInputPanel: textEdit.activeFocusOnPress
71
72     property alias inputMethodHints: textEdit.inputMethodHints
73
74     property bool errorHighlight: false
75
76     property Item platformSipAttributes
77
78     property bool platformEnableEditBubble: true
79
80     property Item platformStyle: TextAreaStyle {}
81     property alias style: root.platformStyle
82
83     property alias platformPreedit: inputMethodObserver.preedit
84
85     onPlatformSipAttributesChanged: {
86         platformSipAttributes.registerInputElement(textEdit)
87     }
88
89     function copy() {
90         textEdit.copy()
91     }
92
93     function paste() {
94         textEdit.paste()
95     }
96
97     function cut() {
98         textEdit.cut()
99     }
100
101     // ensure propagation of forceActiveFocus
102     function forceActiveFocus() {
103         textEdit.forceActiveFocus()
104     }
105
106     function select(start, end) {
107         textEdit.select(start, end)
108     }
109
110     function selectAll() {
111         textEdit.selectAll()
112     }
113
114     function selectWord() {
115         textEdit.selectWord()
116     }
117
118     function positionAt(x, y) {
119         var p = mapToItem(textEdit, x, y);
120         return textEdit.positionAt(p.x, p.y)
121     }
122
123     function positionToRectangle(pos) {
124         var rect = textEdit.positionToRectangle(pos)
125         var point = mapFromItem(textEdit, rect.x, rect.y)
126         rect.x = point.x; rect.y = point.y
127         return rect;
128     }
129
130     function closeSoftwareInputPanel() {
131         console.log("TextArea's function closeSoftwareInputPanel is deprecated. Use function platformCloseSoftwareInputPanel instead.")
132         platformCloseSoftwareInputPanel()
133     }
134
135     function platformCloseSoftwareInputPanel() {
136         inputContext.simulateSipClose();
137         textEdit.closeSoftwareInputPanel();
138     }
139
140     function openSoftwareInputPanel() {
141         console.log("TextArea's function openSoftwareInputPanel is deprecated. Use function platformOpenSoftwareInputPanel instead.")
142         platformOpenSoftwareInputPanel()
143     }
144
145     function platformOpenSoftwareInputPanel() {
146         inputContext.simulateSipOpen();
147         textEdit.openSoftwareInputPanel();
148     }
149
150     Connections {
151         target: platformWindow
152
153         onActiveChanged: {
154             if(platformWindow.active) {
155                 if (!readOnly) {
156                     if (activeFocus) {
157                         platformOpenSoftwareInputPanel();
158                         repositionTimer.running = true;
159                     }
160                 }
161             } else {
162                 if (activeFocus) {
163                     platformCloseSoftwareInputPanel();
164                     Popup.close(textEdit);
165                 }
166             }
167         }
168
169         onAnimatingChanged: {
170             if (!platformWindow.animating && root.activeFocus) {
171                 TextAreaHelper.repositionFlickable(contentMovingAnimation);
172             }
173         }
174     }
175
176     // private
177     property int __preeditDisabledMask: Qt.ImhHiddenText|                       
178                                         Qt.ImhNoPredictiveText|                
179                                         Qt.ImhDigitsOnly|                      
180                                         Qt.ImhFormattedNumbersOnly|             
181                                         Qt.ImhDialableCharactersOnly|           
182                                         Qt.ImhEmailCharactersOnly|              
183                                         Qt.ImhUrlCharactersOnly 
184
185     implicitWidth: platformStyle.defaultWidth
186     implicitHeight: Math.max (UI.FIELD_DEFAULT_HEIGHT,
187                               textEdit.height + (UI.FIELD_DEFAULT_HEIGHT - font.pixelSize))
188
189     onActiveFocusChanged: {
190         if (activeFocus &&
191             !readOnly) {
192             platformOpenSoftwareInputPanel();
193             repositionTimer.running = true;
194         } else if (!activeFocus) {
195             if (!readOnly)
196                 platformCloseSoftwareInputPanel();
197
198             Popup.close(textEdit);            
199         }
200     }
201
202     BorderImage {
203         id: background
204         source: errorHighlight?
205                 platformStyle.backgroundError:
206             readOnly?
207                 platformStyle.backgroundDisabled:
208             textEdit.activeFocus? 
209                 platformStyle.backgroundSelected:
210                 platformStyle.background
211
212         anchors.fill: parent
213         border.left: root.platformStyle.backgroundCornerMargin; border.top: root.platformStyle.backgroundCornerMargin
214         border.right: root.platformStyle.backgroundCornerMargin; border.bottom: root.platformStyle.backgroundCornerMargin
215     }
216
217     Text {
218         id: prompt
219
220         anchors.fill: parent
221         anchors.leftMargin: UI.PADDING_XLARGE
222         anchors.rightMargin: UI.PADDING_XLARGE
223         anchors.topMargin: (UI.FIELD_DEFAULT_HEIGHT - font.pixelSize) / 2
224         anchors.bottomMargin: (UI.FIELD_DEFAULT_HEIGHT - font.pixelSize) / 2
225
226         font: root.platformStyle.textFont
227         color: root.platformStyle.promptTextColor
228         elide: Text.ElideRight
229
230         // opacity for default state
231         opacity:  0.0
232
233         states: [
234             State {
235                 name: "unfocused"
236                 // memory allocation optimization: cursorPosition is checked to minimize displayText evaluations
237                 when: !root.activeFocus && textEdit.cursorPosition == 0 && !textEdit.text && prompt.text && !textEdit.inputMethodComposing
238                 PropertyChanges { target: prompt; opacity: 1.0; }
239             },
240             State {
241                 name: "focused"
242                 // memory allocation optimization: cursorPosition is checked to minimize displayText evaluations
243                 when: root.activeFocus && textEdit.cursorPosition == 0 && !textEdit.text && prompt.text && !textEdit.inputMethodComposing
244                 PropertyChanges { target: prompt; opacity: 0.6; }
245             }
246         ]
247
248         transitions: [
249             Transition {
250                 from: "unfocused"; to: "focused";
251                 reversible: true
252                 SequentialAnimation {
253                     PauseAnimation { duration: 60 }
254                     NumberAnimation { target: prompt; properties: "opacity"; duration: 150 }
255                 }
256             },
257             Transition {
258                 from: "focused"; to: "";
259                 reversible: true
260                 SequentialAnimation {
261                     PauseAnimation { duration:  60 }
262                     NumberAnimation { target: prompt; properties: "opacity"; duration: 100 }
263                 }
264             }
265         ]
266     }
267
268     MouseArea {
269         enabled: !textEdit.activeFocus
270         z: enabled?1:0
271         anchors.fill: parent
272         anchors.margins: UI.TOUCH_EXPANSION_MARGIN
273         onClicked: {
274             if (!textEdit.activeFocus) {
275                 textEdit.forceActiveFocus();
276
277                 // activate to preedit and/or move the cursor
278                 var preeditDisabled = root.inputMethodHints &                   
279                                       root.__preeditDisabledMask
280                 var injectionSucceeded = false;
281                 var mappedMousePos = mapToItem(textEdit, mouseX, mouseY);
282                 var newCursorPosition = textEdit.positionAt(mappedMousePos.x, mappedMousePos.y, TextInput.CursorOnCharacter);
283                 if (!preeditDisabled
284                         && !TextAreaHelper.atSpace(newCursorPosition)
285                         && newCursorPosition != textEdit.text.length
286                         && !(newCursorPosition == 0 || TextAreaHelper.atSpace(newCursorPosition - 1))) {
287                     injectionSucceeded = TextAreaHelper.injectWordToPreedit(newCursorPosition);
288                 }
289                 if (!injectionSucceeded) {
290                     textEdit.cursorPosition=newCursorPosition;
291                 }
292             }
293         }
294     }
295
296     TextEdit {
297         id: textEdit
298
299         // Exposed for the edit bubble
300         property alias preedit: inputMethodObserver.preedit
301         property alias preeditCursorPosition: inputMethodObserver.preeditCursorPosition
302
303         x: UI.PADDING_XLARGE
304         y: (UI.FIELD_DEFAULT_HEIGHT - font.pixelSize) / 2
305         width: parent.width - UI.PADDING_XLARGE * 2
306
307         font: root.platformStyle.textFont
308         color: root.platformStyle.textColor
309         selectByMouse: false
310         selectedTextColor: root.platformStyle.selectedTextColor
311         selectionColor: root.platformStyle.selectionColor
312         mouseSelectionMode: TextInput.SelectWords
313         wrapMode: TextEdit.Wrap
314         persistentSelection: false
315         focus: true
316
317         function updateMagnifierPosition(posX, posY) {
318             var yAdjustment = 0
319             var magnifier = MagnifierPopup.popup;
320             var cursorHeight = textEdit.positionToRectangle(0,0).height;
321             var mappedPos =  mapToItem(magnifier.parent, posX - magnifier.width / 2,
322                                        posY - magnifier.height / 2 - cursorHeight - 70);
323
324             magnifier.xCenter = mapToItem(magnifier.sourceItem, posX, 0).x;
325             magnifier.x = mappedPos.x;
326             if (-root.mapFromItem(magnifier.__rootElement(), 0,0).y - (posY - cursorHeight) < (magnifier.height / 1.5)) {
327                 yAdjustment = Math.max(0,(magnifier.height / 1.5) + root.mapFromItem(magnifier.__rootElement(), 0,0).y - (posY - cursorHeight));
328             } else {
329                 yAdjustment = 0;
330             }
331             magnifier.yCenter = mapToItem(magnifier.sourceItem, 0, posY - cursorHeight + 50).y
332             magnifier.y = mappedPos.y + yAdjustment;
333         }
334
335         Component.onDestruction: {
336             Popup.close(textEdit);
337         }
338
339         onTextChanged: {
340             if(root.activeFocus) {
341                 TextAreaHelper.repositionFlickable(contentMovingAnimation);
342             }
343
344             if (textEdit.preedit == "" && Popup.isOpened(textEdit) && !Popup.isChangingInput())
345                 Popup.close(textEdit);
346         }
347
348         Connections {
349             target: TextAreaHelper.findFlickable(root.parent)
350
351             onContentYChanged: if (root.activeFocus) TextAreaHelper.filteredInputContextUpdate();
352             onContentXChanged: if (root.activeFocus) TextAreaHelper.filteredInputContextUpdate();
353             onMovementEnded: inputContext.update();
354         }
355
356         Connections {
357             target: inputContext
358
359             onSoftwareInputPanelVisibleChanged: {
360                 if (activeFocus)
361                     TextAreaHelper.repositionFlickable(contentMovingAnimation);
362             }
363
364             onSoftwareInputPanelRectChanged: {
365                 if (activeFocus)
366                     TextAreaHelper.repositionFlickable(contentMovingAnimation);
367             }
368         }
369
370         onCursorPositionChanged: {
371             if(!MagnifierPopup.isOpened() && activeFocus) {
372                 TextAreaHelper.repositionFlickable(contentMovingAnimation)
373             }
374
375            if (MagnifierPopup.isOpened() &&
376                Popup.isOpened(textEdit)) {
377                Popup.close(textEdit);
378            } else if ((!mouseFilter.attemptToActivate ||
379                 textEdit.cursorPosition == textEdit.text.length) &&
380                 Popup.isOpened(textEdit)) {
381                 Popup.close(textEdit);
382                 Popup.open(textEdit,
383                            textEdit.positionToRectangle(textEdit.cursorPosition));
384             }
385         }
386
387         onSelectedTextChanged: {
388             if (Popup.isOpened(textEdit) && !Popup.isChangingInput()) {
389                 Popup.close(textEdit);
390             }
391         }
392
393         InputMethodObserver {
394             id: inputMethodObserver
395
396             onPreeditChanged: {
397                 if (Popup.isOpened(textEdit) && !Popup.isChangingInput()) {
398                     Popup.close(textEdit);
399                 }
400             }
401
402         }
403
404         Timer {
405             id: repositionTimer
406             interval: 350
407             onTriggered: TextAreaHelper.repositionFlickable(contentMovingAnimation)
408         }
409
410         PropertyAnimation {
411             id: contentMovingAnimation
412             property: "contentY"
413             duration: 200
414             easing.type: Easing.InOutCubic
415         }
416
417         MouseFilter {
418             id: mouseFilter
419             anchors.fill: parent
420             anchors.leftMargin:  UI.TOUCH_EXPANSION_MARGIN - UI.PADDING_XLARGE
421             anchors.rightMargin:  UI.TOUCH_EXPANSION_MARGIN - UI.PADDING_MEDIUM
422             anchors.topMargin: UI.TOUCH_EXPANSION_MARGIN - (UI.FIELD_DEFAULT_HEIGHT - font.pixelSize) / 2
423             anchors.bottomMargin:  UI.TOUCH_EXPANSION_MARGIN - (UI.FIELD_DEFAULT_HEIGHT - font.pixelSize) / 2
424
425             property bool attemptToActivate: false
426             property bool pressOnPreedit
427
428            property variant editBubblePosition: Qt.point(0,0) 
429
430             onPressed: {
431                 var mousePosition = textEdit.positionAt(mouse.x,mouse.y,TextEdit.CursorOnCharacter);
432                 pressOnPreedit = textEdit.cursorPosition==mousePosition
433                 var preeditDisabled = root.inputMethodHints &                  
434                                       root.__preeditDisabledMask
435
436                 attemptToActivate = !pressOnPreedit && !root.readOnly && !preeditDisabled && root.activeFocus &&
437                                     !(mousePosition == 0 || TextAreaHelper.atSpace(mousePosition - 1) || TextAreaHelper.atSpace(mousePosition));
438                 mouse.filtered = true;
439             }
440
441             onHorizontalDrag: {
442                 // possible pre-edit word have to be committed before selection
443                 if (root.activeFocus || root.readOnly) {
444                     inputContext.reset()                    
445                     parent.selectByMouse = true
446                     attemptToActivate = false
447                 }
448             }
449
450             onPressAndHold:{
451                 // possible pre-edit word have to be commited before showing the magnifier
452                 if ((root.text != "" || inputMethodObserver.preedit != "") && root.activeFocus) {
453                     inputContext.reset()
454                     attemptToActivate = false
455                     parent.selectByMouse = false
456                     MagnifierPopup.open(root);
457                     var magnifier = MagnifierPopup.popup;
458                     parent.cursorPosition = parent.positionAt(mouse.x,mouse.y)
459                     parent.updateMagnifierPosition(mouse.x,mouse.y)
460                     root.z = Number.MAX_VALUE
461                 }
462             }
463
464             onReleased:{                
465                 if (MagnifierPopup.isOpened()) {
466                     MagnifierPopup.close();
467                     TextAreaHelper.repositionFlickable(contentMovingAnimation);
468                 }
469
470                 if (attemptToActivate)
471                     inputContext.reset();
472
473                 var newCursorPosition = textEdit.positionAt(mouse.x,mouse.y,TextEdit.CursorOnCharacter);
474                 editBubblePosition = textEdit.positionToRectangle(newCursorPosition);
475
476                 if (attemptToActivate) {
477                     var beforeText = textEdit.text;
478
479                     textEdit.cursorPosition = newCursorPosition;
480                     var injectionSucceeded = false;
481
482                     if (!TextAreaHelper.atSpace(newCursorPosition)                             
483                              && newCursorPosition != textEdit.text.length) {
484                         injectionSucceeded = TextAreaHelper.injectWordToPreedit(newCursorPosition);
485                     }
486                     if (injectionSucceeded) {
487                         mouse.filtered=true;
488                         if (textEdit.preedit.length >=1 && textEdit.preedit.length <= 4)
489                             editBubblePosition = textEdit.positionToRectangle(textEdit.cursorPosition);
490                     } else {
491                         textEdit.text=beforeText;
492                         textEdit.cursorPosition=newCursorPosition;
493                     }
494                     attemptToActivate = false;
495                 } else if (!parent.selectByMouse) {
496                     if (!pressOnPreedit) inputContext.reset();
497                     textEdit.cursorPosition = textEdit.positionAt(mouse.x,mouse.y,TextEdit.CursorOnCharacter);
498                 }
499                 parent.selectByMouse = false;
500             }
501             onFinished: {
502                 if (root.activeFocus && platformEnableEditBubble) {
503                     if (textEdit.preedit.length == 0)
504                         editBubblePosition = textEdit.positionToRectangle(textEdit.cursorPosition);
505                     Popup.open(textEdit,editBubblePosition);
506                 }
507             }
508             onMousePositionChanged: {
509                if (MagnifierPopup.isOpened() && !parent.selectByMouse) {
510                     var pos = textEdit.positionAt (mouse.x,mouse.y)
511                     var posNextLine = textEdit.positionAt (mouse.x, mouse.y + 1)
512                     var posPrevLine = textEdit.positionAt (mouse.x, mouse.y - 1)
513                     if (!(Math.abs(posNextLine - pos) > 1 ||
514                         Math.abs(posPrevLine - pos) > 1)) {
515                         parent.cursorPosition = pos
516                     }
517                     parent.updateMagnifierPosition(mouse.x,mouse.y);
518                 }
519             }
520             onDoubleClicked: {
521                 // possible pre-edit word have to be committed before selection
522                 inputContext.reset()
523                 parent.selectByMouse = true
524                 attemptToActivate = false
525             }
526         }
527     }
528
529
530
531     InverseMouseArea {
532         anchors.fill: parent
533         anchors.margins: UI.TOUCH_EXPANSION_MARGIN
534         enabled: root.activeFocus
535
536         onClickedOutside: {
537             if (Popup.isOpened(textEdit) && ((mouseX > Popup.geometry().left && mouseX < Popup.geometry().right) &&
538                                            (mouseY > Popup.geometry().top && mouseY < Popup.geometry().bottom))) {
539                 return;
540             }
541
542             root.parent.focus = true;
543         }
544     }
545 }