~michael-sheldon/ubuntu-keyboard/fix-oxide-dismiss-test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Ubuntu Keyboard Test Suite
# Copyright (C) 2013, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from collections import defaultdict

from ubuntu_keyboard.emulators.keypad import KeyPad

from time import sleep
import logging
import os

import ubuntuuitoolkit as toolkit
from autopilot.input import Pointer, Touch
from autopilot.introspection import (
    get_proxy_object_for_existing_process,
    ProcessSearchError
)
from autopilot.introspection.dbus import StateNotFoundError


logger = logging.getLogger(__name__)


class KeyboardState:
    character = "CHARACTERS"
    symbol = "SYMBOLS"


class Keyboard(object):
    """Emulator that provides the OSK as an input backend."""

    _action_to_label = {
        'SHIFT': 'shift',
        '\b': 'backspace',
        'ABC': 'symbols',
        '?123': 'symbols',
        ' ': 'space',
        '\n': 'return',
        'Enter': 'return',
        'Backspace': 'backspace',
        'Space': 'space',
        'Shift': 'shift',
    }

    __maliit = None

    def __init__(self, pointer=None):
        try:
            self.keyboard = self.maliit.select_single(
                "QQuickItem",
                objectName="ubuntuKeyboard"
            )
        except ValueError as e:
            e.args += (
                "There was more than one Keyboard object found, aborting.",
            )
            raise
        except StateNotFoundError:
            logger.error(
                "Unable to find the Ubuntu Keyboard object within the "
                "maliit server, aborting."
            )
            raise

        self._keyboard_container = self.keyboard.select_single(
            "KeyboardContainer"
        )

        self._stored_active_keypad_name = None
        self._active_keypad = None

        self._keys_position = defaultdict(dict)
        self._keys_contained = defaultdict(dict)

        if pointer is None:
            self.pointer = Pointer(Touch.create())
        else:
            self.pointer = pointer

    @property
    def maliit(self):
        # We cache __mallit_server as a class attribute because
        # get_proxy_object_for_existing_process clears backends for proxy
        # objects, this means that if this is called twice within a test the
        # first keyboard object created has now lost all its _Backends.
        if Keyboard.__maliit is None:
            try:
                Keyboard.__maliit = get_proxy_object_for_existing_process(
                    connection_name='org.maliit.server',
                    emulator_base=toolkit.UbuntuUIToolkitCustomProxyObjectBase
                )

                if Keyboard.__maliit is None:
                    raise RuntimeError("Maliit Server could not be found.")
            except ProcessSearchError as e:
                e.args += (
                    "Unable to find maliit-server dbus object. Has it been "
                    "started with introspection enabled?",
                )
                raise
        return Keyboard.__maliit

    def dismiss(self):
        """Swipe the keyboard down to hide it.

        :raises: *AssertionError* if the state.wait_for fails meaning that the
         keyboard failed to hide.

        """
        if self.is_available():
            x, y, h, w = self._keyboard_container.globalRect
            x_pos = int(w / 2)
            start_y = y + int(h / 2)
            end_y = y + h
            self.pointer.drag(x_pos, start_y, x_pos, end_y)

            self.keyboard.state.wait_for("HIDDEN")

    def is_available(self):
        """Returns true if the keyboard is shown and ready to use."""
        return (self.keyboard.state == "SHOWN")

    # Much like is_available, but attempts to wait for the keyboard to be
    # ready.
    def wait_for_keyboard_ready(self, timeout=10):
        """Waits for *timeout* for the keyboard to be ready and returns
        true. Returns False if the keyboard fails to be considered ready within
        the alloted time.

        """
        try:
            self.keyboard.state.wait_for("SHOWN", timeout=timeout)
            return True
        except AssertionError:
            return False

    def press_key(self, key, capslock_switch=False, long_press=False,
                  slide_offset=None):
        """Tap on the key with the internal pointer

        :params key: String containing the text of the key to tap.

        :raises: *RuntimeError* if the keyboard is not available and thus not
          ready to be used.
        :raises: *ValueError* if the supplied key cannot be found on any of
          the the current keyboards layouts.
        """
        if not self.is_available():
            raise RuntimeError("Keyboard is not on screen")

        key = self._translate_key(key)

        req_keypad = KeyboardState.character
        if capslock_switch:
            req_key_state = "CAPSLOCK"
        else:
            req_key_state = self._keypad_contains_key(req_keypad, key)
        if req_key_state is None:
            req_keypad = KeyboardState.symbol
            req_key_state = self._keypad_contains_key(req_keypad, key)

        if req_key_state is None:
            raise ValueError("Key '%s' was not found on the keyboard" % key)

        key_pos = self._get_key_pos_from_keypad(req_keypad, key)
        self._show_keypad(req_keypad)
        self._change_keypad_to_state(req_key_state)

        if slide_offset is not None:
            self._select_extended_key(key_pos, slide_offset)
        elif long_press:
            self._long_press_key(key_pos)
        else:
            self._tap_key(key_pos)

    def type(self, string, delay=0.1):
        """Type the string *string* with a delay of *delay* between each key
        press

        .. note:: The delay provides a minimum delay, it may take longer
        between each press as the keyboard shifts between states etc.

        Only 'normal' or single characters can be typed this way.

        :raises: *ValueError* if one of the the supplied keys cannot be
          found on any of the the current keyboards layouts.

        """
        for char in string:
            self.press_key(char)
            sleep(delay)

    @property
    def current_state(self):
        return self.keyboard.state

    @property
    def active_keypad_state(self):
        return self._keyboard_container.activeKeypadState

    @property
    def active_keypad(self):
        need_to_update = False
        if self._active_keypad is None:
            need_to_update = True
        else:
            try:
                # Check if the current keypad object still exists.
                self._active_keypad.enabled
            except StateNotFoundError:
                need_to_update = True

        if (
            need_to_update
            or self._stored_active_keypad_name != self._current_keypad_name
        ):
            self._stored_active_keypad_name = self._current_keypad_name
            logger.debug("Keypad lookup")
            self._active_keypad = self._keypad_loader.select_single(KeyPad)
        return self._active_keypad

    @property
    def _keypad_loader(self):
        return self.maliit.select_single(
            "QQuickLoader", objectName='characterKeyPadLoader')

    @property
    def _plugin_source(self):
        return self._keypad_loader.source

    @property
    def _current_keypad_name(self):
        return self._keyboard_container.state

    def _update_details_for_keypad(self, keypad_name):
        self._show_keypad(keypad_name)

        contained, positions = self.active_keypad.get_key_details()
        self._keys_contained[self._keyboard_container.state] = contained
        self._keys_position[self._keyboard_container.state] = positions

    def _keypad_contains_key(self, keypad_name, key):
        """Returns the keypad state that key is found in.

        Returns either a KeyPadState if the key is found on the provided keypad
        or None if not found.

        """
        if self._keypad_details_expired(keypad_name):
            self._update_details_for_keypad(keypad_name)

        return self._keys_contained[keypad_name].get(key, None)

    def _get_key_pos_from_keypad(self, keypad_name, key):
        """Returns the position of the key if it is found on that keypad or
        None if it is not.

        """
        if self._keypad_details_expired(keypad_name):
            self._update_details_for_keypad(keypad_name)

        return self._keys_position[keypad_name].get(key, None)

    def _show_keypad(self, keypad_name):
        if self._current_keypad_name == keypad_name:
            return

        key_pos = self._get_key_pos_from_keypad(
            self._current_keypad_name,
            "symbols"
        )
        self._tap_key(key_pos)
        self._current_keypad_name.wait_for(keypad_name)
        self.active_keypad.opacity.wait_for(1.0)

    def _change_keypad_to_state(self, state):
        if self._keyboard_container.activeKeypadState == state:
            return

        key_pos = self._get_key_pos_from_keypad(
            self._current_keypad_name,
            "shift"
        )

        if key_pos is None:
            # Not all layouts have a shift key
            return

        self._tap_key(key_pos)
        self._keyboard_container.activeKeypadState.wait_for(state)
        self.active_keypad.opacity.wait_for(1.0)

    def _tap_key(self, key_rect, pointer=None):
        if pointer is None:
            pointer = Pointer(Touch.create())
        pointer.click_object(key_rect)

    def _long_press_key(self, key_rect, pointer=None):
        if pointer is None:
            pointer = Pointer(Touch.create())
        pointer.move(
            key_rect.x + key_rect.w / 2.0, key_rect.y + key_rect.h / 2.0)
        pointer.press()
        sleep(0.5)
        pointer.release()

    def _select_extended_key(self, key_rect, offset, pointer=None):
        if pointer is None:
            pointer = Pointer(Touch.create())

        gu = float(os.environ.get('GRID_UNIT_PX', 8))

        pointer.drag(
            key_rect.x + key_rect.w / 2.0,
            key_rect.y + key_rect.h / 2.0,
            key_rect.x + key_rect.w / 2.0 + offset,
            key_rect.y + key_rect.h / 2.0,
            rate=2.77 * gu, time_between_events=2)

    def _keypad_details_expired(self, keypad_name):
        return (
            self._keys_contained.get(keypad_name) is None
        )

    def _translate_key(self, label):
        """Get the label for a 'special key' (i.e. space) so that it can be
        addressed and clicked.

        """
        return Keyboard._action_to_label.get(label, label)