Dekko
InputOverlay.qml
1 import QtQuick 2.4
2 import Lomiri.Components 1.3
3 
4 // Modified version of InputHandler from the UITK
5 MultiPointTouchArea {
6  id: overlay
7 
8  // PROPERTIES
9  property Item root
10  property TextEdit textEdit
11  property Flickable flickable
12  property Item visibleArea: Item {
13  parent: flickable
14  anchors.fill: parent
15  }
16  property real lineSpacing: units.dp(3)
17  property real lineSize: textEdit.font.pixelSize + lineSpacing
18  property point frameDistance: Qt.point(flickable.x, flickable.y)
19  property string oldText: ""
20  property var flickableList: new Array()
21  property bool textChanged: false
22  property var popover
23  property bool suppressReleaseEvent: false
24  property int pressedPosition: -1
25  property int moveStarts: -1
26  property int moveEnds: -1
27  property bool doubleTapInProgress: doubleTap.running
28  property Item cursorPositionCursor: null
29  property Item selectionStartCursor: null
30  property Item selectionEndCursor: null
31 
32  // SIGNALS
33  signal pressAndHold(int pos, bool fromTouch)
34  signal tap(int pos)
35  signal textModified()
36 
37  //SLOTS
38 
39  touchPoints: TouchPoint {
40  id: touchPoint
41  function touch() {
42  longTap.restart();
43  if (!doubleTap.running) {
44  doubleTap.restart();
45  } else if (doubleTap.tapCount > 0) {
46  doubleTap.running = false;
47  handleDblClick(touchPoint, true);
48  }
49  }
50  function untouch() {
51  longTap.running = false;
52  }
53  function reset() {
54  longTap.running = false;
55  doubleTap.running = false;
56  }
57  }
58 
59  onPressed: handlePressed(touchPoints[0])
60  onReleased: handleReleased(touchPoints[0])
61 
62  // Mouse handling
63  Mouse.forwardTo: [root]
64  Mouse.onPressed: handlePressed(mouse)
65  Mouse.onReleased: handleReleased(mouse)
66  Mouse.onPositionChanged: handleMove(mouse)
67  Mouse.onDoubleClicked: handleDblClick(mouse)
68  Keys.onMenuPressed: overlay.pressAndHold(input.cursorPosition, false);
69  // PageUp and PageDown handling
70  Keys.onPressed: {
71  if (event.key === Qt.Key_PageUp && event.modifiers === Qt.NoModifier) {
72  movePage(false);
73  } else if (event.key === Qt.Key_PageDown && event.modifiers === Qt.NoModifier) {
74  movePage(true);
75  }
76  }
77 
78  // FUNCTIONS
79  function activateInput() {
80  if (!textEdit.activeFocus) {
81  textEdit.forceActiveFocus();
82  }
83  showInputPanel();
84  }
85 
86  function showInputPanel() {
87  if (!LomiriApplication.inputMethod.visible) {
88  LomiriApplication.inputMethod.show();
89  }
90  textChanged = false;
91  }
92 
93  // ensures the text cursorRectangle is always in the internal Flickable's visible area
94  function ensureVisible(rect)
95  {
96  var headHeight = root.headerHeight + root.frameSpacing + units.gu(2)
97  if (flickable.moving || flickable.flicking)
98  return;
99  if (flickable.contentX >= rect.x)
100  flickable.contentX = rect.x;
101  else if (flickable.contentX + flickable.width <= rect.x + rect.width)
102  flickable.contentX = rect.x + rect.width - flickable.width;
103  if (flickable.contentY >= rect.y)
104  flickable.contentY = rect.y;
105  else if (flickable.contentY + (flickable.height - headHeight) <= rect.y + rect.height)
106  flickable.contentY = rect.y + rect.height - (flickable.height - headHeight);
107  }
108 
109  // returns the cursor position from x,y pair
110  function unadulteratedCursorPosition(x, y) {
111  return textEdit.positionAt(x, y, TextInput.CursorOnCharacter);
112  }
113 
114  // returns the cursor position taking frame into account
115  function cursorPosition(x, y) {
116  var frameSpacing = root.frameSpacing;
117  var cursorPosition = unadulteratedCursorPosition(x, y);
118 // if (cursorPosition === 0)
119 // cursorPosition = unadulteratedCursorPosition(x + frameSpacing, y + frameSpacing);
120 // if (cursorPosition === textEdit.text.length)
121 // cursorPosition = unadulteratedCursorPosition(x - frameSpacing, y - frameSpacing);
122  return cursorPosition
123  }
124 
125  // returns the mouse position
126  function mousePosition(mouse) {
127  return cursorPosition(mouse.x, mouse.y);
128  }
129  // checks whether the position is in the selected text
130  function positionInSelection(pos) {
131  return (textEdit.selectionStart !== textEdit.selectionEnd)
132  && (pos >= Math.min(textEdit.selectionStart, textEdit.selectionEnd))
133  && (pos <= Math.max(textEdit.selectionStart, textEdit.selectionEnd));
134  }
135 
136  // check whether the mouse is inside a selected text area
137  function mouseInSelection(mouse) {
138  var pos = mousePosition(mouse);
139  return positionInSelection(pos);
140  }
141 
142  // selects text
143  function selectText(mouse) {
144  state = "select";
145  moveEnds = mousePosition(mouse);
146  if (moveStarts < 0) {
147  moveStarts = moveEnds;
148  }
149  textEdit.select(moveStarts, moveEnds);
150  }
151 
152  // returns the first Flickable parent of a given item
153  function firstFlickableParent(item) {
154  var p = item ? item.parent : null;
155  while (p && !p.hasOwnProperty("flicking")) {
156  p = p.parent;
157  }
158  return p;
159  }
160 
161  // focuses the input if not yet focused, and shows the context menu
162  function openContextMenu(mouse, noAutoselect) {
163  var pos = mousePosition(mouse);
164  if (!root.focus || !mouseInSelection(mouse)) {
165  activateInput();
166  textEdit.cursorPosition = pressedPosition = mousePosition(mouse);
167  if (!noAutoselect) {
168  textEdit.selectWord();
169  }
170  }
171  // open context menu at the cursor position
172  overlay.pressAndHold(textEdit.cursorPosition, mouse.touch);
173  // if opened with left press (touch falls into this criteria as well), we need to set state to inactive
174  // so the mouse moves won't result in selected text loss/change
175  if (mouse.button === Qt.LeftButton) {
176  state = "inactive";
177  }
178  }
179 
180  // disables interactive Flickable parents, stops at the first non-interactive flickable.
181  function toggleFlickablesInteractive(turnOn) {
182  flickable.interactive = turnOn
183  }
184 
185  // moves the specified position, called by the cursor handler
186  // positioner = "currentPosition/selectionStart/selectionEnd"
187  function positionCaret(positioner, x, y) {
188  if (positioner === "cursorPosition") {
189  textEdit[positioner] = cursorPosition(x, y);
190  } else {
191  var pos = cursorPosition(x, y);
192  if (positioner === "selectionStart" && (pos < textEdit.selectionEnd)) {
193  textEdit.select(pos, input.selectionEnd);
194  } else if (positioner === "selectionEnd" && (pos > textEdit.selectionStart)) {
195  textEdit.select(textEdit.selectionStart, pos);
196  }
197  }
198  }
199 
200  // moves the cursor one page forward with or without positioning the cursor
201  function movePage(forward) {
202  var cx = textEdit.cursorRectangle.x;
203  var cy = textEdit.cursorRectangle.y;
204  if (forward) {
205  cy += visibleArea.height;
206 
207  } else {
208  cy -= visibleArea.height;
209  }
210  textEdit.cursorPosition = cursorPosition(cx, cy);
211  }
212 
213  // touch and mous handling
214  function handlePressed(event) {
215  if (event.touch) {
216  // we do not have longTap or double tap, therefore we need to generate those
217  event.touch();
218  } else {
219  // consume event so it does not get forwarded to the input
220  event.accepted = true;
221  }
222  // remember pressed position as we need it when entering into selection state
223  pressedPosition = mousePosition(event);
224  }
225  function handleReleased(event) {
226  if (event.touch) {
227  event.untouch();
228  }
229  if ((!root.focus && !root.activeFocusOnPress) || suppressReleaseEvent === true) {
230  suppressReleaseEvent = false;
231  return;
232  }
233 
234  activateInput();
235  if (state === "" || event.touch) {
236  textEdit.cursorPosition = mousePosition(event);
237  }
238  moveStarts = moveEnds = -1;
239  state = "";
240  // check if we get right-click from the frame or the area that has no text
241  if (event.button === Qt.RightButton) {
242  // open the popover
243  overlay.pressAndHold(textEdit.cursorPosition, event.touch);
244  } else {
245  overlay.tap(textEdit.cursorPosition);
246  }
247  }
248  function handleMove(event) {
249  // leave if not focus, not the left button or not in select state
250  if (!textEdit.activeFocus || (!event.touch && event.button !== Qt.LeftButton) || !textEdit.selectByMouse) {
251  return;
252  }
253  selectText(event);
254  }
255  function handleDblClick(event) {
256  if (root.selectByMouse) {
257  openContextMenu(event, false);
258  // turn selection state temporarily so the selection is not cleared on release
259  state = "selection";
260  suppressReleaseEvent = true;
261  }
262  }
263 
264 
265  // COMPONENTS
266 
267  // brings the state back to default when the component looses focus
268  // and ensures input has active focus when component regains focus
269  Connections {
270  target: root
271  ignoreUnknownSignals: true
272  onActiveFocusChanged: {
273  textEdit.focus = root.activeFocus;
274  }
275  onKeyNavigationFocusChanged: {
276  if (root.keyNavigationFocus) {
277  textEdit.forceActiveFocus();
278  }
279  }
280  onFocusChanged: {
281  LomiriApplication.inputMethod.commit()
282  state = (root.focus) ? "" : "inactive";
283  if (root.focus) {
284  textEdit.forceActiveFocus()
285  }
286  }
287  onVisibleChanged: {
288  if (!root.visible)
289  root.focus = false;
290  }
291  }
292 
293  // input specific signals
294  Connections {
295  target: textEdit
296  onCursorRectangleChanged: ensureVisible(textEdit.cursorRectangle)
297  onTextChanged: {
298  textChanged = true;
299  if (oldText != textEdit.text) {
300  textModified()
301  oldText = textEdit.text
302  }
303  }
304  // make sure we show the OSK
305  onActiveFocusChanged: {
306  if (!textEdit.activeFocus && popover)
307  PopupUtils.close(popover);
308  showInputPanel();
309  }
310  }
311 
312  // inner or outer Flickable controlling
313  Connections {
314  target: flickable
315  // turn scrolling state on
316  onFlickStarted: toggleScrollingState(true)
317  onMovementStarted: toggleScrollingState(true)
318  // reset to default state
319  onMovementEnded: toggleScrollingState(false)
320 
321  function toggleScrollingState(turnOn) {
322  if (!root.focus) {
323  return;
324  }
325  overlay.state = (turnOn) ? "scrolling" : ""
326  }
327  }
328 
329  // cursors to use when text is selected
330  Connections {
331  target: textEdit
332  onSelectedTextChanged: {
333  if (textEdit.selectedText !== "") {
334  if (!selectionStartCursor) {
335  selectionStartCursor = textEdit.cursorDelegate.createObject(
336  textEdit, {
337  "positionProperty": "selectionStart",
338  "handler": overlay,
339  }
340  );
341  moveSelectionCursor(selectionStartCursor);
342  selectionEndCursor = textEdit.cursorDelegate.createObject(
343  textEdit, {
344  "positionProperty": "selectionEnd",
345  "handler": overlay,
346  }
347  );
348  moveSelectionCursor(selectionEndCursor);
349  }
350  } else {
351  if (selectionStartCursor) {
352  selectionStartCursor.destroy();
353  selectionStartCursor = null;
354  selectionEndCursor.destroy();
355  selectionEndCursor = null;
356  }
357  }
358  }
359  onSelectionStartChanged: moveSelectionCursor(selectionStartCursor, true);
360  onSelectionEndChanged: moveSelectionCursor(selectionEndCursor, true);
361 
362  function moveSelectionCursor(cursor, updateProperty) {
363  if (!cursor) {
364  return;
365  }
366  // workaround for https://bugreports.qt-project.org/browse/QTBUG-38704
367  // selectedTextChanged signal is not emitted for TextEdit when selectByMouse is false
368  if (updateProperty && QuickUtils.className(textEdit) === "QQuickTextEdit") {
369  textEdit.selectedTextChanged();
370  }
371 
372  var pos = textEdit.positionToRectangle(textEdit[cursor.positionProperty]);
373  cursor.x = pos.x;
374  cursor.y = pos.y;
375  cursor.height = pos.height;
376  ensureVisible(pos);
377  }
378  }
379 
380  // right button handling
381  MouseArea {
382  anchors.fill: parent
383  acceptedButtons: Qt.RightButton
384  // trigger pressAndHold
385  onReleased: openContextMenu(mouse, true)
386  cursorShape: Qt.IBeamCursor
387  }
388 
389  Timer {
390  id: doubleTap
391  property int tapCount: 0
392  interval: 400
393  onRunningChanged: {
394  tapCount = running
395  }
396  }
397 
398  Timer {
399  id: longTap
400  interval: 800
401  onTriggered: {
402  // do not open context menu if the input is not focus
403  if (!root.focus) {
404  return;
405  }
406 
407  // do not open context menu if this is scrolling
408  if (touchPoint.startY - touchPoint.y < -units.gu(2))
409  return;
410 
411  openContextMenu(touchPoint, false);
412  suppressReleaseEvent = true;
413  }
414  }
415 
416  // STATES
417  states: [
418  // override default state to turn on the saved Flickable interactive mode
419  State {
420  name: ""
421  StateChangeScript {
422  // restore interactive for all Flickable parents
423  script: toggleFlickablesInteractive(true);
424  }
425  },
426  State {
427  name: "inactive"
428  },
429  State {
430  name: "scrolling"
431  StateChangeScript {
432  script: {
433  // stop touch timers
434  touchPoint.reset();
435  }
436  }
437  },
438  State {
439  name: "select"
440  // during select state all the flickables are blocked (interactive = false)
441  // we can use scroller here as we need to disable the outer scroller too!
442  PropertyChanges {
443  target: flickable
444  interactive: false
445  }
446  StateChangeScript {
447  script: {
448  // turn off interactive for all parent flickables
449  toggleFlickablesInteractive(false);
450  if (!positionInSelection(pressedPosition)) {
451  textEdit.cursorPosition = pressedPosition;
452  }
453  }
454  }
455  }
456  ]
457 }