Path: blob/trunk/py/selenium/webdriver/common/bidi/input.py
1864 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.1617import math18from dataclasses import dataclass, field19from typing import Any, Optional, Union2021from selenium.webdriver.common.bidi.common import command_builder22from selenium.webdriver.common.bidi.session import Session232425class PointerType:26"""Represents the possible pointer types."""2728MOUSE = "mouse"29PEN = "pen"30TOUCH = "touch"3132VALID_TYPES = {MOUSE, PEN, TOUCH}333435class Origin:36"""Represents the possible origin types."""3738VIEWPORT = "viewport"39POINTER = "pointer"404142@dataclass43class ElementOrigin:44"""Represents an element origin for input actions."""4546type: str47element: dict4849def __init__(self, element_reference: dict):50self.type = "element"51self.element = element_reference5253def to_dict(self) -> dict:54"""Convert the ElementOrigin to a dictionary."""55return {"type": self.type, "element": self.element}565758@dataclass59class PointerParameters:60"""Represents pointer parameters for pointer actions."""6162pointer_type: str = PointerType.MOUSE6364def __post_init__(self):65if self.pointer_type not in PointerType.VALID_TYPES:66raise ValueError(f"Invalid pointer type: {self.pointer_type}. Must be one of {PointerType.VALID_TYPES}")6768def to_dict(self) -> dict:69"""Convert the PointerParameters to a dictionary."""70return {"pointerType": self.pointer_type}717273@dataclass74class PointerCommonProperties:75"""Common properties for pointer actions."""7677width: int = 178height: int = 179pressure: float = 0.080tangential_pressure: float = 0.081twist: int = 082altitude_angle: float = 0.083azimuth_angle: float = 0.08485def __post_init__(self):86if self.width < 1:87raise ValueError("width must be at least 1")88if self.height < 1:89raise ValueError("height must be at least 1")90if not (0.0 <= self.pressure <= 1.0):91raise ValueError("pressure must be between 0.0 and 1.0")92if not (0.0 <= self.tangential_pressure <= 1.0):93raise ValueError("tangential_pressure must be between 0.0 and 1.0")94if not (0 <= self.twist <= 359):95raise ValueError("twist must be between 0 and 359")96if not (0.0 <= self.altitude_angle <= math.pi / 2):97raise ValueError("altitude_angle must be between 0.0 and π/2")98if not (0.0 <= self.azimuth_angle <= 2 * math.pi):99raise ValueError("azimuth_angle must be between 0.0 and 2π")100101def to_dict(self) -> dict:102"""Convert the PointerCommonProperties to a dictionary."""103result: dict[str, Any] = {}104if self.width != 1:105result["width"] = self.width106if self.height != 1:107result["height"] = self.height108if self.pressure != 0.0:109result["pressure"] = self.pressure110if self.tangential_pressure != 0.0:111result["tangentialPressure"] = self.tangential_pressure112if self.twist != 0:113result["twist"] = self.twist114if self.altitude_angle != 0.0:115result["altitudeAngle"] = self.altitude_angle116if self.azimuth_angle != 0.0:117result["azimuthAngle"] = self.azimuth_angle118return result119120121# Action classes122@dataclass123class PauseAction:124"""Represents a pause action."""125126duration: Optional[int] = None127128@property129def type(self) -> str:130return "pause"131132def to_dict(self) -> dict:133"""Convert the PauseAction to a dictionary."""134result: dict[str, Any] = {"type": self.type}135if self.duration is not None:136result["duration"] = self.duration137return result138139140@dataclass141class KeyDownAction:142"""Represents a key down action."""143144value: str = ""145146@property147def type(self) -> str:148return "keyDown"149150def to_dict(self) -> dict:151"""Convert the KeyDownAction to a dictionary."""152return {"type": self.type, "value": self.value}153154155@dataclass156class KeyUpAction:157"""Represents a key up action."""158159value: str = ""160161@property162def type(self) -> str:163return "keyUp"164165def to_dict(self) -> dict:166"""Convert the KeyUpAction to a dictionary."""167return {"type": self.type, "value": self.value}168169170@dataclass171class PointerDownAction:172"""Represents a pointer down action."""173174button: int = 0175properties: Optional[PointerCommonProperties] = None176177@property178def type(self) -> str:179return "pointerDown"180181def to_dict(self) -> dict:182"""Convert the PointerDownAction to a dictionary."""183result: dict[str, Any] = {"type": self.type, "button": self.button}184if self.properties:185result.update(self.properties.to_dict())186return result187188189@dataclass190class PointerUpAction:191"""Represents a pointer up action."""192193button: int = 0194195@property196def type(self) -> str:197return "pointerUp"198199def to_dict(self) -> dict:200"""Convert the PointerUpAction to a dictionary."""201return {"type": self.type, "button": self.button}202203204@dataclass205class PointerMoveAction:206"""Represents a pointer move action."""207208x: float = 0209y: float = 0210duration: Optional[int] = None211origin: Optional[Union[str, ElementOrigin]] = None212properties: Optional[PointerCommonProperties] = None213214@property215def type(self) -> str:216return "pointerMove"217218def to_dict(self) -> dict:219"""Convert the PointerMoveAction to a dictionary."""220result: dict[str, Any] = {"type": self.type, "x": self.x, "y": self.y}221if self.duration is not None:222result["duration"] = self.duration223if self.origin is not None:224if isinstance(self.origin, ElementOrigin):225result["origin"] = self.origin.to_dict()226else:227result["origin"] = self.origin228if self.properties:229result.update(self.properties.to_dict())230return result231232233@dataclass234class WheelScrollAction:235"""Represents a wheel scroll action."""236237x: int = 0238y: int = 0239delta_x: int = 0240delta_y: int = 0241duration: Optional[int] = None242origin: Optional[Union[str, ElementOrigin]] = Origin.VIEWPORT243244@property245def type(self) -> str:246return "scroll"247248def to_dict(self) -> dict:249"""Convert the WheelScrollAction to a dictionary."""250result: dict[str, Any] = {251"type": self.type,252"x": self.x,253"y": self.y,254"deltaX": self.delta_x,255"deltaY": self.delta_y,256}257if self.duration is not None:258result["duration"] = self.duration259if self.origin is not None:260if isinstance(self.origin, ElementOrigin):261result["origin"] = self.origin.to_dict()262else:263result["origin"] = self.origin264return result265266267# Source Actions268@dataclass269class NoneSourceActions:270"""Represents a sequence of none actions."""271272id: str = ""273actions: list[PauseAction] = field(default_factory=list)274275@property276def type(self) -> str:277return "none"278279def to_dict(self) -> dict:280"""Convert the NoneSourceActions to a dictionary."""281return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]}282283284@dataclass285class KeySourceActions:286"""Represents a sequence of key actions."""287288id: str = ""289actions: list[Union[PauseAction, KeyDownAction, KeyUpAction]] = field(default_factory=list)290291@property292def type(self) -> str:293return "key"294295def to_dict(self) -> dict:296"""Convert the KeySourceActions to a dictionary."""297return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]}298299300@dataclass301class PointerSourceActions:302"""Represents a sequence of pointer actions."""303304id: str = ""305parameters: Optional[PointerParameters] = None306actions: list[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = field(307default_factory=list308)309310def __post_init__(self):311if self.parameters is None:312self.parameters = PointerParameters()313314@property315def type(self) -> str:316return "pointer"317318def to_dict(self) -> dict:319"""Convert the PointerSourceActions to a dictionary."""320result: dict[str, Any] = {321"type": self.type,322"id": self.id,323"actions": [action.to_dict() for action in self.actions],324}325if self.parameters:326result["parameters"] = self.parameters.to_dict()327return result328329330@dataclass331class WheelSourceActions:332"""Represents a sequence of wheel actions."""333334id: str = ""335actions: list[Union[PauseAction, WheelScrollAction]] = field(default_factory=list)336337@property338def type(self) -> str:339return "wheel"340341def to_dict(self) -> dict:342"""Convert the WheelSourceActions to a dictionary."""343return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]}344345346@dataclass347class FileDialogInfo:348"""Represents file dialog information from input.fileDialogOpened event."""349350context: str351multiple: bool352element: Optional[dict] = None353354@classmethod355def from_dict(cls, data: dict) -> "FileDialogInfo":356"""Creates a FileDialogInfo instance from a dictionary.357358Parameters:359-----------360data: A dictionary containing the file dialog information.361362Returns:363-------364FileDialogInfo: A new instance of FileDialogInfo.365"""366return cls(context=data["context"], multiple=data["multiple"], element=data.get("element"))367368369# Event Class370class FileDialogOpened:371"""Event class for input.fileDialogOpened event."""372373event_class = "input.fileDialogOpened"374375@classmethod376def from_json(cls, json):377"""Create FileDialogInfo from JSON data."""378return FileDialogInfo.from_dict(json)379380381class Input:382"""383BiDi implementation of the input module.384"""385386def __init__(self, conn):387self.conn = conn388self.subscriptions = {}389self.callbacks = {}390391def perform_actions(392self,393context: str,394actions: list[Union[NoneSourceActions, KeySourceActions, PointerSourceActions, WheelSourceActions]],395) -> None:396"""Performs a sequence of user input actions.397398Parameters:399-----------400context: The browsing context ID where actions should be performed.401actions: A list of source actions to perform.402"""403params = {"context": context, "actions": [action.to_dict() for action in actions]}404self.conn.execute(command_builder("input.performActions", params))405406def release_actions(self, context: str) -> None:407"""Releases all input state for the given context.408409Parameters:410-----------411context: The browsing context ID to release actions for.412"""413params = {"context": context}414self.conn.execute(command_builder("input.releaseActions", params))415416def set_files(self, context: str, element: dict, files: list[str]) -> None:417"""Sets files for a file input element.418419Parameters:420-----------421context: The browsing context ID.422element: The element reference (script.SharedReference).423files: A list of file paths to set.424"""425params = {"context": context, "element": element, "files": files}426self.conn.execute(command_builder("input.setFiles", params))427428def add_file_dialog_handler(self, handler):429"""Add a handler for file dialog opened events.430431Parameters:432-----------433handler: Callback function that takes a FileDialogInfo object.434435Returns:436--------437int: Callback ID for removing the handler later.438"""439# Subscribe to the event if not already subscribed440if FileDialogOpened.event_class not in self.subscriptions:441session = Session(self.conn)442self.conn.execute(session.subscribe(FileDialogOpened.event_class))443self.subscriptions[FileDialogOpened.event_class] = []444445# Add callback - the callback receives the parsed FileDialogInfo directly446callback_id = self.conn.add_callback(FileDialogOpened, handler)447448self.subscriptions[FileDialogOpened.event_class].append(callback_id)449self.callbacks[callback_id] = handler450451return callback_id452453def remove_file_dialog_handler(self, callback_id: int) -> None:454"""Remove a file dialog handler.455456Parameters:457-----------458callback_id: The callback ID returned by add_file_dialog_handler.459"""460if callback_id in self.callbacks:461del self.callbacks[callback_id]462463if FileDialogOpened.event_class in self.subscriptions:464if callback_id in self.subscriptions[FileDialogOpened.event_class]:465self.subscriptions[FileDialogOpened.event_class].remove(callback_id)466467# If no more callbacks for this event, unsubscribe468if not self.subscriptions[FileDialogOpened.event_class]:469session = Session(self.conn)470self.conn.execute(session.unsubscribe(FileDialogOpened.event_class))471del self.subscriptions[FileDialogOpened.event_class]472473self.conn.remove_callback(FileDialogOpened, callback_id)474475476