tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

selection.py (13230B)


      1 # -*- coding: utf-8 -*-
      2 # This Source Code Form is subject to the terms of the Mozilla Public
      3 # License, v. 2.0. If a copy of the MPL was not distributed with this
      4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      5 
      6 from marionette_driver.marionette import Actions, errors
      7 
      8 
      9 class CaretActions(Actions):
     10    def __init__(self, marionette):
     11        super(CaretActions, self).__init__(marionette)
     12        self._reset_action_chain()
     13 
     14    def _reset_action_chain(self):
     15        # Release the action so that two consecutive clicks won't become a
     16        # double-click.
     17        self.release()
     18 
     19        self.mouse_chain = self.sequence(
     20            "pointer", "pointer_id", {"pointerType": "mouse"}
     21        )
     22        self.key_chain = self.sequence("key", "keyboard_id")
     23 
     24    def move(self, element, x, y, duration=None):
     25        """Queue a pointer_move action.
     26 
     27        :param element: an element where its top-left corner is the origin of
     28                        the coordinates.
     29        :param x: Destination x-axis coordinate in CSS pixels.
     30        :param y: Destination y-axis coordinate in CSS pixels.
     31        :param duration: Number of milliseconds over which to distribute the
     32                         move. If None, remote end defaults to 0.
     33 
     34        """
     35        rect = element.rect
     36        el_x, el_y = rect["x"], rect["y"]
     37 
     38        # Add the element's top-left corner (el_x, el_y) to make the coordinate
     39        # relative to the viewport.
     40        dest_x, dest_y = int(el_x + x), int(el_y + y)
     41 
     42        self.mouse_chain.pointer_move(dest_x, dest_y, duration=duration)
     43        return self
     44 
     45    def click(self, element=None):
     46        """Queue a click action.
     47 
     48        If an element is given, move the pointer to that element first,
     49        otherwise click current pointer coordinates.
     50 
     51        :param element: Optional element to click.
     52 
     53        """
     54        self.mouse_chain.click(element=element)
     55        return self
     56 
     57    def flick(self, element, x1, y1, x2, y2, duration=200):
     58        """Queue a flick gesture on the target element.
     59 
     60        :param element: The element to perform the flick gesture on. Its
     61                        top-left corner is the origin of the coordinates.
     62        :param x1: Starting x-axis coordinate of the flick in CSS Pixels.
     63        :param y1: Starting y-axis coordinate of the flick in CSS Pixels.
     64        :param x2: Ending x-axis coordinate of the flick in CSS Pixels.
     65        :param y2: Ending y-axis coordinate of the flick in CSS Pixels.
     66 
     67        """
     68        self.move(element, x1, y1, duration=0)
     69        self.mouse_chain.pointer_down()
     70        self.move(element, x2, y2, duration=duration)
     71        self.mouse_chain.pointer_up()
     72        return self
     73 
     74    def send_keys(self, keys):
     75        """Perform a keyDown and keyUp action for each character in `keys`.
     76 
     77        :param keys: String of keys to perform key actions with.
     78 
     79        """
     80        self.key_chain.send_keys(keys)
     81        return self
     82 
     83    def perform(self):
     84        """Perform the action chain built so far to the server side for execution
     85        and clears the current chain of actions.
     86 
     87        Warning: This method performs all the mouse actions before all the key
     88        actions!
     89 
     90        """
     91        self.mouse_chain.perform()
     92        self.key_chain.perform()
     93        self._reset_action_chain()
     94 
     95 
     96 class SelectionManager(object):
     97    """Interface for manipulating the selection and carets of the element.
     98 
     99    We call the blinking cursor (nsCaret) as cursor, and call AccessibleCaret as
    100    caret for short.
    101 
    102    Simple usage example:
    103 
    104    ::
    105 
    106        element = marionette.find_element(By.ID, 'input')
    107        sel = SelectionManager(element)
    108        sel.move_caret_to_front()
    109 
    110    """
    111 
    112    def __init__(self, element):
    113        self.element = element
    114 
    115    def _input_or_textarea(self):
    116        """Return True if element is either <input> or <textarea>."""
    117        return self.element.tag_name in ("input", "textarea")
    118 
    119    def js_selection_cmd(self):
    120        """Return a command snippet to get selection object.
    121 
    122        If the element is <input> or <textarea>, return the selection object
    123        associated with it. Otherwise, return the current selection object.
    124 
    125        Note: "element" must be provided as the first argument to
    126        execute_script().
    127 
    128        """
    129        if self._input_or_textarea():
    130            # We must unwrap sel so that DOMRect could be returned to Python
    131            # side.
    132            return """var sel = arguments[0].editor.selection;"""
    133        else:
    134            return """var sel = window.getSelection();"""
    135 
    136    def move_cursor_by_offset(self, offset, backward=False):
    137        """Move cursor in the element by character offset.
    138 
    139        :param offset: Move the cursor to the direction by offset characters.
    140        :param backward: Optional, True to move backward; Default to False to
    141         move forward.
    142 
    143        """
    144        cmd = (
    145            self.js_selection_cmd()
    146            + """
    147              for (let i = 0; i < {0}; ++i) {{
    148                  sel.modify("move", "{1}", "character");
    149              }}
    150              """.format(
    151                offset, "backward" if backward else "forward"
    152            )
    153        )
    154 
    155        self.element.marionette.execute_script(
    156            cmd, script_args=(self.element,), sandbox="system"
    157        )
    158 
    159    def move_cursor_to_front(self):
    160        """Move cursor in the element to the front of the content."""
    161        if self._input_or_textarea():
    162            cmd = """arguments[0].setSelectionRange(0, 0);"""
    163        else:
    164            cmd = """var sel = window.getSelection();
    165                  sel.collapse(arguments[0].firstChild, 0);"""
    166 
    167        self.element.marionette.execute_script(
    168            cmd, script_args=(self.element,), sandbox=None
    169        )
    170 
    171    def move_cursor_to_end(self):
    172        """Move cursor in the element to the end of the content."""
    173        if self._input_or_textarea():
    174            cmd = """var len = arguments[0].value.length;
    175                  arguments[0].setSelectionRange(len, len);"""
    176        else:
    177            cmd = """var sel = window.getSelection();
    178                  sel.collapse(arguments[0].lastChild, arguments[0].lastChild.length);"""
    179 
    180        self.element.marionette.execute_script(
    181            cmd, script_args=(self.element,), sandbox=None
    182        )
    183 
    184    def selection_rect_list(self, idx):
    185        """Return the selection's DOMRectList object for the range at given idx.
    186 
    187        If the element is either <input> or <textarea>, return the DOMRectList of
    188        the range at given idx of the selection within the element. Otherwise,
    189        return the DOMRectList of the of the range at given idx of current selection.
    190 
    191        """
    192        cmd = (
    193            self.js_selection_cmd()
    194            + """return sel.getRangeAt({}).getClientRects();""".format(idx)
    195        )
    196        return self.element.marionette.execute_script(
    197            cmd, script_args=(self.element,), sandbox="system"
    198        )
    199 
    200    def range_count(self):
    201        """Get selection's range count"""
    202        cmd = self.js_selection_cmd() + """return sel.rangeCount;"""
    203        return self.element.marionette.execute_script(
    204            cmd, script_args=(self.element,), sandbox="system"
    205        )
    206 
    207    def _selection_location_helper(self, location_type):
    208        """Return the start and end location of the selection in the element.
    209 
    210        Return a tuple containing two pairs of (x, y) coordinates of the start
    211        and end locations in the element. The coordinates are relative to the
    212        top left-hand corner of the element. Both ltr and rtl directions are
    213        considered.
    214 
    215        """
    216        range_count = self.range_count()
    217        if range_count <= 0:
    218            raise errors.MarionetteException(
    219                "Expect at least one range object in Selection, but found nothing!"
    220            )
    221 
    222        # FIXME (Bug 1682382): We shouldn't need the retry for-loops if
    223        # selection_rect_list() can reliably return a valid list.
    224        retry_times = 3
    225        for _ in range(retry_times):
    226            try:
    227                first_rect_list = self.selection_rect_list(0)
    228                first_rect = first_rect_list["0"]
    229                break
    230            except KeyError:
    231                continue
    232        else:
    233            raise errors.MarionetteException(
    234                "Expect at least one rect in the first range, but found nothing!"
    235            )
    236 
    237        for _ in range(retry_times):
    238            try:
    239                # Making a selection over some non-selectable elements can
    240                # create multiple ranges.
    241                last_rect_list = (
    242                    first_rect_list
    243                    if range_count == 1
    244                    else self.selection_rect_list(range_count - 1)
    245                )
    246                last_list_length = last_rect_list["length"]
    247                last_rect = last_rect_list[str(last_list_length - 1)]
    248                break
    249            except KeyError:
    250                continue
    251        else:
    252            raise errors.MarionetteException(
    253                "Expect at least one rect in the last range, but found nothing!"
    254            )
    255 
    256        origin_x, origin_y = self.element.rect["x"], self.element.rect["y"]
    257 
    258        if self.element.get_property("dir") == "rtl":  # such as Arabic
    259            start_pos, end_pos = "right", "left"
    260        else:
    261            start_pos, end_pos = "left", "right"
    262 
    263        # Calculate y offset according to different needs.
    264        if location_type == "center":
    265            start_y_offset = first_rect["height"] / 2.0
    266            end_y_offset = last_rect["height"] / 2.0
    267        elif location_type == "caret":
    268            # Selection carets' tip are below the bottom of the two ends of the
    269            # selection. Add 5px to y should be sufficient to locate them.
    270            caret_tip_y_offset = 5
    271            start_y_offset = first_rect["height"] + caret_tip_y_offset
    272            end_y_offset = last_rect["height"] + caret_tip_y_offset
    273        else:
    274            start_y_offset = end_y_offset = 0
    275 
    276        caret1_x = first_rect[start_pos] - origin_x
    277        caret1_y = first_rect["top"] + start_y_offset - origin_y
    278        caret2_x = last_rect[end_pos] - origin_x
    279        caret2_y = last_rect["top"] + end_y_offset - origin_y
    280 
    281        return ((caret1_x, caret1_y), (caret2_x, caret2_y))
    282 
    283    def selection_location(self):
    284        """Return the start and end location of the selection in the element.
    285 
    286        Return a tuple containing two pairs of (x, y) coordinates of the start
    287        and end of the selection. The coordinates are relative to the top
    288        left-hand corner of the element. Both ltr and rtl direction are
    289        considered.
    290 
    291        """
    292        return self._selection_location_helper("center")
    293 
    294    def carets_location(self):
    295        """Return a pair of the two carets' location.
    296 
    297        Return a tuple containing two pairs of (x, y) coordinates of the two
    298        carets' tip. The coordinates are relative to the top left-hand corner of
    299        the element. Both ltr and rtl direction are considered.
    300 
    301        """
    302        return self._selection_location_helper("caret")
    303 
    304    def cursor_location(self):
    305        """Return the blanking cursor's center location within the element.
    306 
    307        Return (x, y) coordinates of the cursor's center relative to the top
    308        left-hand corner of the element.
    309 
    310        """
    311        return self._selection_location_helper("center")[0]
    312 
    313    def first_caret_location(self):
    314        """Return the first caret's location.
    315 
    316        Return (x, y) coordinates of the first caret's tip relative to the top
    317        left-hand corner of the element.
    318 
    319        """
    320        return self.carets_location()[0]
    321 
    322    def second_caret_location(self):
    323        """Return the second caret's location.
    324 
    325        Return (x, y) coordinates of the second caret's tip relative to the top
    326        left-hand corner of the element.
    327 
    328        """
    329        return self.carets_location()[1]
    330 
    331    def select_all(self):
    332        """Select all the content in the element."""
    333        if self._input_or_textarea():
    334            cmd = """var len = arguments[0].value.length;
    335                  arguments[0].focus();
    336                  arguments[0].setSelectionRange(0, len);"""
    337        else:
    338            cmd = """var range = document.createRange();
    339                  range.setStart(arguments[0].firstChild, 0);
    340                  range.setEnd(arguments[0].lastChild, arguments[0].lastChild.length);
    341                  var sel = window.getSelection();
    342                  sel.removeAllRanges();
    343                  sel.addRange(range);"""
    344 
    345        self.element.marionette.execute_script(
    346            cmd, script_args=(self.element,), sandbox=None
    347        )
    348 
    349    @property
    350    def content(self):
    351        """Return all the content of the element."""
    352        if self._input_or_textarea():
    353            return self.element.get_property("value")
    354        else:
    355            return self.element.text
    356 
    357    @property
    358    def selected_content(self):
    359        """Return the selected portion of the content in the element."""
    360        cmd = self.js_selection_cmd() + """return sel.toString();"""
    361        return self.element.marionette.execute_script(
    362            cmd, script_args=(self.element,), sandbox="system"
    363        )