Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,391 @@
# SPDX-FileCopyrightText: 2019-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import datetime
import string
import bpy
event_types = tuple(
e.identifier.lower()
for e in bpy.types.Event.bl_rna.properties["type"].enum_items_static
)
del bpy
# We don't normally care about which one.
event_types_alias = {
"ctrl": "left_ctrl",
"shift": "left_shift",
"alt": "left_alt",
# Collides with Python keywords.
"delete": "del",
}
# Note, we could add support for other keys using control characters,
# for example: `\xF12` could be used for the F12 key.
#
# Besides this, we could encode symbols into a regular string using our own syntax
# which can mix regular text and key symbols.
#
# At the moment this doesn't seem necessary, no need to add it.
event_types_text = (
('ZERO', "0", False),
('ONE', "1", False),
('TWO', "2", False),
('THREE', "3", False),
('FOUR', "4", False),
('FIVE', "5", False),
('SIX', "6", False),
('SEVEN', "7", False),
('EIGHT', "8", False),
('NINE', "9", False),
('ONE', "!", True),
('TWO', "@", True),
('THREE', "#", True),
('FOUR', "$", True),
('FIVE', "%", True),
('SIX', "^", True),
('SEVEN', "&", True),
('EIGHT', "*", True),
('NINE', "(", True),
('ZERO', ")", True),
('MINUS', "-", False),
('MINUS', "_", True),
('EQUAL', "=", False),
('EQUAL', "+", True),
('ACCENT_GRAVE', "`", False),
('ACCENT_GRAVE', "~", True),
('LEFT_BRACKET', "[", False),
('LEFT_BRACKET', "{", True),
('RIGHT_BRACKET', "]", False),
('RIGHT_BRACKET', "}", True),
('SEMI_COLON', ";", False),
('SEMI_COLON', ":", True),
('PERIOD', ".", False),
('PERIOD', ">", True),
('COMMA', ",", False),
('COMMA', "<", True),
('QUOTE', "'", False),
('QUOTE', '"', True),
('SLASH', "/", False),
('SLASH', "?", True),
('BACK_SLASH', "\\", False),
('BACK_SLASH', "|", True),
*((ch_upper, ch, False) for (ch_upper, ch) in zip(string.ascii_uppercase, string.ascii_lowercase)),
*((ch, ch, True) for ch in string.ascii_uppercase),
('SPACE', " ", False),
('RET', "\n", False),
('TAB', "\t", False),
)
event_types_text_from_char = {ch: (ty, is_shift) for (ty, ch, is_shift) in event_types_text}
event_types_text_from_event = {(ty, is_shift): ch for (ty, ch, is_shift) in event_types_text}
class _EventBuilder:
__slots__ = (
"_shared_event_gen",
"_event_type",
"_parent",
)
def __init__(self, event_gen, ty):
self._shared_event_gen = event_gen
self._event_type = ty
self._parent = None
def __call__(self, count=1):
assert count >= 0
for _ in range(count):
self.tap()
return self._shared_event_gen
def _key_press_release(self, do_press=False, do_release=False, unicode_override=None):
assert (do_press or do_release)
keys_held = self._shared_event_gen._event_types_held
build_keys = []
e = self
while e is not None:
build_keys.append(e._event_type.upper())
e = e._parent
build_keys.reverse()
events = [None, None]
for i, value in enumerate(('PRESS', 'RELEASE')):
if value == 'RELEASE':
build_keys.reverse()
for event_type in build_keys:
if value == 'PRESS':
keys_held.add(event_type)
else:
keys_held.remove(event_type)
if (not do_press) and value == 'PRESS':
continue
if (not do_release) and value == 'RELEASE':
continue
shift = 'LEFT_SHIFT' in keys_held or 'RIGHT_SHIFT' in keys_held
ctrl = 'LEFT_CTRL' in keys_held or 'RIGHT_CTRL' in keys_held
shift = 'LEFT_SHIFT' in keys_held or 'RIGHT_SHIFT' in keys_held
alt = 'LEFT_ALT' in keys_held or 'RIGHT_ALT' in keys_held
oskey = 'OSKEY' in keys_held
hyper = 'HYPER' in keys_held
unicode = None
if value == 'PRESS':
if ctrl is False and alt is False and oskey is False:
if unicode_override is not None:
unicode = unicode_override
else:
unicode = event_types_text_from_event.get((event_type, shift))
if unicode is None and shift:
# Some keys don't care about shift
unicode = event_types_text_from_event.get((event_type, False))
event = self._shared_event_gen.window.event_simulate(
type=event_type,
value=value,
unicode=unicode,
shift=shift,
ctrl=ctrl,
alt=alt,
oskey=oskey,
hyper=hyper,
x=self._shared_event_gen._mouse_co[0],
y=self._shared_event_gen._mouse_co[1],
)
events[i] = event
return tuple(events)
def tap(self):
return self._key_press_release(do_press=True, do_release=True)
def press(self):
return self._key_press_release(do_press=True)[0]
def release(self):
return self._key_press_release(do_release=True)[1]
def cursor_motion(self, coords):
coords = list(coords)
self._shared_event_gen.cursor_position_set(*coords[0], move=True)
yield
event = self.press()
shift = event.shift
ctrl = event.ctrl
shift = event.shift
alt = event.alt
oskey = event.oskey
hyper = event.hyper
yield
for x, y in coords:
self._shared_event_gen.window.event_simulate(
type='MOUSEMOVE',
value='NOTHING',
unicode=None,
shift=shift,
ctrl=ctrl,
alt=alt,
oskey=oskey,
hyper=hyper,
x=x,
y=y
)
yield
self._shared_event_gen.cursor_position_set(x, y, move=False)
self.release()
yield
def __getattr__(self, attr):
attr = event_types_alias.get(attr, attr)
if attr in event_types:
e = _EventBuilder(self._shared_event_gen, attr)
e._parent = self
return e
raise Exception(f"{attr!r} not found in {event_types!r}")
class EventGenerate:
__slots__ = (
"window",
"_mouse_co",
"_event_types_held",
)
def __init__(self, window):
self.window = window
self._mouse_co = [0, 0]
self._event_types_held = set()
self.cursor_position_set(window.width // 2, window.height // 2)
def cursor_position_set(self, x, y, move=False):
self._mouse_co[:] = x, y
if move:
self.window.event_simulate(
type='MOUSEMOVE',
value='NOTHING',
x=x,
y=y,
)
def text(self, text):
""" Type in entire phrases. """
for ch in text:
ty, shift = event_types_text_from_char[ch]
ty = ty.lower()
if shift:
eb = getattr(_EventBuilder(self, 'left_shift'), ty)
else:
eb = _EventBuilder(self, ty)
eb.tap()
return self
def text_unicode(self, text):
# Since the only purpose of this key-press is to enter text
# the key can be almost anything, use a key which isn't likely to be assigned to any other action.
#
# If it were possible `EVT_UNKNOWNKEY` would be most correct
# as dead keys map to this and still enter text.
ty_dummy = 'F24'
for ch in text:
eb = _EventBuilder(self, ty_dummy)
eb._key_press_release(do_press=True, do_release=True, unicode_override=ch)
return self
def __getattr__(self, attr):
attr = event_types_alias.get(attr, attr)
if attr in event_types:
return _EventBuilder(self, attr)
raise Exception(f"{attr!r} not found in {event_types!r}")
def __del__(self):
if self._event_types_held:
print("'__del__' with keys held:", repr(self._event_types_held))
def run(
event_iter, *,
on_error=None,
on_exit=None,
on_step_command_pre=None,
on_step_command_post=None,
):
import bpy
TICKS = 4 # 3 works, 4 to be on the safe side.
# If we try to handle events this many times
TICKS_HANDLING_BREAK_MAX = 256
def event_step():
# Handle `is_event_handling_break` here so we don't incorrectly detect
# consecutive `is_event_handling_break` based on other functions exiting early.
is_event_handling_break = bpy.context.window_manager.is_event_handling_break
if is_event_handling_break:
if event_step._ticks_handling_break_consecutive > TICKS_HANDLING_BREAK_MAX:
raise RuntimeError(
"window_manager.is_event_handling_break set {:d} times, may be an event handling bug!".format(
TICKS_HANDLING_BREAK_MAX,
)
)
event_step._ticks_handling_break_consecutive += 1
else:
event_step._ticks_handling_break_consecutive = 0
# Run once 'TICKS' is reached.
if event_step._ticks < TICKS:
event_step._ticks += 1
return 0.0
event_step._ticks = 0
# Wait for any pending event queue break to be cleared before advancing,
# otherwise events injected by the next step may be deferred unexpectedly.
if is_event_handling_break:
return 0.0
if on_step_command_pre:
if event_step.run_events.gi_frame is not None:
import shlex
import subprocess
subprocess.call(
shlex.split(
on_step_command_pre.replace(
"{file}", event_step.run_events.gi_frame.f_code.co_filename,
).replace(
"{line}", str(event_step.run_events.gi_frame.f_lineno),
)
)
)
try:
val = next(event_step.run_events, Ellipsis)
except Exception:
import traceback
traceback.print_exc()
if on_error is not None:
on_error()
if on_exit is not None:
on_exit()
return None
if on_step_command_post:
if event_step.run_events.gi_frame is not None:
import shlex
import subprocess
subprocess.call(
shlex.split(
on_step_command_post.replace(
"{file}", event_step.run_events.gi_frame.f_code.co_filename,
).replace(
"{line}", str(event_step.run_events.gi_frame.f_lineno),
)
)
)
if isinstance(val, EventGenerate) or val is None:
return 0.0
elif isinstance(val, datetime.timedelta):
return val.total_seconds()
elif val is Ellipsis:
if on_exit is not None:
on_exit()
return None
else:
raise Exception(f"{val!r} of type {type(val)!r} not supported")
event_step.run_events = iter(event_iter)
event_step._ticks = 0
event_step._ticks_handling_break_consecutive = 0
# Persistent so this keeps working when tests load a blend file.
bpy.app.timers.register(event_step, first_interval=0.0, persistent=True)
def setup_default_preferences(preferences):
""" Set preferences useful for automation.
"""
preferences.view.show_splash = False
preferences.view.smooth_view = 0
preferences.view.use_save_prompt = False
preferences.filepaths.use_auto_save_temporary_files = False

View File

@@ -0,0 +1,196 @@
# SPDX-FileCopyrightText: 2019-2026 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
__all__ = (
"call_menu",
"call_operator",
"cursor_motion_data_x",
"cursor_motion_data_y",
"cursor_motion_data_xy",
"cursor_motion_data_circle",
"get_area_center",
"get_area_center_from_spacetype",
"get_window_area_by_type",
"get_window_size_in_pixels",
"idle_until",
"keep_open",
"test_window",
)
def test_window(window_index=0):
"""
Get window with associated event simulator and tests.
"""
import unittest
from .easy_keys import EventGenerate
import bpy
window = bpy.data.window_managers[0].windows[window_index]
return (
EventGenerate(window),
unittest.TestCase(),
window,
)
def call_operator(e, text: str):
"""
Call operator by name.
"""
yield e.f3()
yield e.text(text)
yield e.ret()
def call_menu(e, text: str):
"""
Call operator through menu.
"""
yield e.f3()
yield e.text_unicode(text.replace(" -> ", " \u25b8 "))
yield e.ret()
def get_window_area_by_type(window, space_type):
"""
Get first area of the specified space type in a window.
"""
for area in window.screen.areas:
if area.type == space_type:
return area
def get_area_center(area):
"""
Get coordinate in the center of an area, e.g. for placing the cursor.
"""
return (
area.x + area.width // 2,
area.y + area.height // 2,
)
def get_area_center_from_spacetype(window, space_type):
"""
Get coordinate in the center of the first area with the given spacetype.
"""
area = get_window_area_by_type(window, space_type)
if area is None:
raise Exception("Space Type {!r} not found".format(space_type))
return get_area_center(area)
def get_window_size_in_pixels(window):
"""
Get window size that can be used for positioning a cursor.
"""
import sys
size = window.width, window.height
# macOS window size is a multiple of the pixel_size.
if sys.platform == "darwin":
from bpy import context
# The value is always rounded to an int, so converting to an int is safe here.
pixel_size = int(context.preferences.system.pixel_size)
size = size[0] * pixel_size, size[1] * pixel_size
return size
def idle_until(until, idle=1 / 60, timeout=1.0):
"""
Idle while the internal event loop runs until a specified condition is true.
This should be used sparingly as it may represent some other failure
condition inside Blender. Currently, it is used to:
- Test completion and cancellation of operators that use jobs.
- Multi window undo tests that need separate view layer (see #148903).
Note: In practice, the timeout value of 1.0 seconds should be more than enough
for all cases. In testing with a fixed, constant delay, the tests succeeded
with a timeout of 1/6th of a second.
:param until: lambda to check the condition of after each sleep
:param idle: how long to idle between checks of the `until` lambda.
Defaults to 60Hz due to common refresh rates.
:param timeout: the max time in seconds that this busy wait will execute.
:return:
"""
import datetime
import time
start_time = time.time()
current_time = time.time()
while current_time - start_time < timeout and not until():
yield datetime.timedelta(seconds=idle)
current_time = time.time()
def keep_open():
"""
Only for development, handy so we can quickly keep the window open while testing.
"""
import bpy
bpy.app.use_event_simulate = False
def cursor_motion_data_x(window, margin=0.2):
"""
Generate a range of (x,y) positions in screen space, centered vertically in the window
from left to right.
:param margin: Percentage of left and right window space to leave unused
"""
size = get_window_size_in_pixels(window)
return [
(x, size[1] // 2) for x in
range(int(size[0] * margin), int(size[0] * (1.0 - margin)), 80)
]
def cursor_motion_data_y(window, margin=0.2):
"""
Generate a range of (x,y) positions in screen space, centered horizontally in the window
from bottom to top.
:param margin: Percentage of top and bottom window space to leave unused
"""
size = get_window_size_in_pixels(window)
return [
(size[0] // 2, y) for y in
range(int(size[1] * margin), int(size[1] * (1.0 - margin)), 80)
]
def cursor_motion_data_xy(window, margin=0.2):
"""
Generate a range of (x,y) positions in screen space from bottom left to top right
:param margin: Percentage of window space to leave unused
"""
size = get_window_size_in_pixels(window)
return [
(p, p) for p in
range(int(size[0] * margin), int(size[0] * (1.0 - margin)), 80)
]
def cursor_motion_data_circle(center, radius):
"""
Generate a range of (x,y) positions in screen space as a circle.
:param center: The center of the circle
:param radius: The radius of the circle
"""
import sys
from math import sin, cos, pi
if sys.platform == "darwin":
from bpy import context
# The value is always rounded to an int, so converting to an int is safe here.
radius = radius * int(context.preferences.system.pixel_size)
steps = 20
angles = [(i / steps) * 2.0 * pi for i in range(steps)]
angles.append(0.0)
return [
(int(center[0] + -radius * sin(phi)), int(center[1] + radius * cos(phi))) for phi in angles
]