Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test undo/redo behaviour of text input widgets on macOS. #2151

Merged
merged 5 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio

import pytest
from java import dynamic_proxy
from pytest import approx

Expand Down Expand Up @@ -231,6 +232,12 @@ def is_hidden(self):
def has_focus(self):
return self.widget.app._impl.native.getCurrentFocus() == self.native

async def undo(self):
pytest.skip("Undo not supported on this platform")

async def redo(self):
pytest.skip("Redo not supported on this platform")


def find_view_by_type(root, cls):
assert isinstance(root, View)
Expand Down
9 changes: 6 additions & 3 deletions android/tests_backend/widgets/numberinput.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pytest import xfail
import pytest

from .textinput import TextInputProbe

Expand All @@ -16,7 +16,10 @@ def clear_input(self):
self.native.setText("")

async def increment(self):
xfail("This backend doesn't support stepped increments")
pytest.xfail("This backend doesn't support stepped increments")

async def decrement(self):
xfail("This backend doesn't support stepped increments")
pytest.xfail("This backend doesn't support stepped increments")

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
4 changes: 4 additions & 0 deletions android/tests_backend/widgets/textinput.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from java import jclass

from android.os import SystemClock
Expand Down Expand Up @@ -80,3 +81,6 @@ async def type_character(self, char):
0, # metaState
)
)

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
1 change: 1 addition & 0 deletions changes/2151.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Text input widgets on macOS now support undo and redo.
1 change: 1 addition & 0 deletions cocoa/src/toga_cocoa/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def create(self):

self.native_text.editable = True
self.native_text.selectable = True
self.native_text.allowsUndo = True
self.native_text.verticallyResizable = True
self.native_text.horizontallyResizable = False
self.native_text.usesAdaptiveColorMappingForDarkAppearance = True
Expand Down
13 changes: 13 additions & 0 deletions cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rubicon.objc import NSPoint

from toga.colors import TRANSPARENT
from toga_cocoa.keys import NSEventModifierFlagCommand, NSEventModifierFlagShift
from toga_cocoa.libs import NSEvent, NSEventType

from ..fonts import FontMixin
Expand Down Expand Up @@ -111,6 +112,7 @@ async def type_character(self, char, modifierFlags=0):
# Convert the requested character into a Cocoa keycode.
# This table is incomplete, but covers all the basics.
key_code = {
"<backspace>": 51,
"<esc>": 53,
" ": 49,
"\n": 36,
Expand Down Expand Up @@ -142,6 +144,9 @@ async def type_character(self, char, modifierFlags=0):
"z": 6,
}.get(char.lower(), 0)

if modifierFlags:
char = None

# This posts a single keyDown followed by a keyUp, matching "normal" keyboard operation.
await self.post_event(
NSEvent.keyEventWithType(
Expand Down Expand Up @@ -194,3 +199,11 @@ async def mouse_event(
),
delay=delay,
)

async def undo(self):
await self.type_character("z", modifierFlags=NSEventModifierFlagCommand)

async def redo(self):
await self.type_character(
"z", modifierFlags=NSEventModifierFlagCommand | NSEventModifierFlagShift
)
5 changes: 4 additions & 1 deletion cocoa/tests_backend/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from toga.colors import TRANSPARENT
from toga_cocoa.libs import NSScrollView, NSTextView
from toga_cocoa.libs import NSRange, NSScrollView, NSTextView

from .base import SimpleProbe
from .properties import toga_alignment, toga_color
Expand Down Expand Up @@ -96,3 +96,6 @@ def vertical_scroll_position(self):
async def wait_for_scroll_completion(self):
# No animation associated with scroll, so this is a no-op
pass

def set_cursor_at_end(self):
self.native.selectedRange = NSRange(len(self.value), 0)
4 changes: 4 additions & 0 deletions cocoa/tests_backend/widgets/numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from toga.colors import TRANSPARENT
from toga_cocoa.libs import (
NSEventType,
NSRange,
NSStepper,
NSTextField,
NSTextView,
Expand Down Expand Up @@ -107,3 +108,6 @@ def has_focus(self):
return isinstance(self.native.window.firstResponder, NSTextView) and (
self.native_input.window.firstResponder.delegate == self.native_input
)

def set_cursor_at_end(self):
self.native_input.currentEditor().selectedRange = NSRange(len(self.value), 0)
4 changes: 4 additions & 0 deletions cocoa/tests_backend/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from toga.constants import RIGHT
from toga_cocoa.libs import (
NSLeftTextAlignment,
NSRange,
NSRightTextAlignment,
NSTextField,
NSTextView,
Expand Down Expand Up @@ -82,3 +83,6 @@ def has_focus(self):
return isinstance(self.native.window.firstResponder, NSTextView) and (
self.native.window.firstResponder.delegate == self.native
)

def set_cursor_at_end(self):
self.native.currentEditor().selectedRange = NSRange(len(self.value), 0)
8 changes: 8 additions & 0 deletions gtk/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import asyncio
from threading import Event

import pytest

from toga_gtk.libs import Gdk, Gtk

from ..fonts import FontMixin
Expand Down Expand Up @@ -167,3 +169,9 @@ def event_handled(widget, e):
# caused by typing a character doesn't fully propegate. A
# short delay fixes this.
await asyncio.sleep(0.04)

async def undo(self):
pytest.skip("Undo not supported on this platform")

async def redo(self):
pytest.skip("Redo not supported on this platform")
5 changes: 5 additions & 0 deletions gtk/tests_backend/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from toga_gtk.libs import Gtk

from .base import SimpleProbe
Expand Down Expand Up @@ -113,3 +115,6 @@ def vertical_scroll_position(self):

async def wait_for_scroll_completion(self):
pass

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
5 changes: 5 additions & 0 deletions gtk/tests_backend/widgets/numberinput.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from toga.constants import JUSTIFY, LEFT
from toga_gtk.libs import Gtk

Expand Down Expand Up @@ -45,3 +47,6 @@ def assert_vertical_alignment(self, expected):
@property
def readonly(self):
return not self.native.get_property("editable")

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
5 changes: 5 additions & 0 deletions gtk/tests_backend/widgets/textinput.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from toga.constants import JUSTIFY, LEFT
from toga_gtk.libs import Gtk

Expand Down Expand Up @@ -47,3 +49,6 @@ def assert_vertical_alignment(self, expected):
@property
def readonly(self):
return not self.native.get_property("editable")

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
7 changes: 7 additions & 0 deletions iOS/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from rubicon.objc import ObjCClass

from toga_iOS.libs import UIApplication
Expand Down Expand Up @@ -166,3 +167,9 @@ async def type_character(self, char):
self.native.insertText(char)
else:
self.native.insertText("")

async def undo(self):
pytest.skip("Undo not supported on this platform")

async def redo(self):
pytest.skip("Redo not supported on this platform")
9 changes: 6 additions & 3 deletions iOS/tests_backend/widgets/numberinput.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pytest import xfail
import pytest
from rubicon.objc import NSRange

from toga_iOS.libs import UITextField
Expand All @@ -19,10 +19,10 @@ def value(self):
return str(self.native.text)

async def increment(self):
xfail("iOS doesn't support stepped increments")
pytest.xfail("iOS doesn't support stepped increments")

async def decrement(self):
xfail("iOS doesn't support stepped increments")
pytest.xfail("iOS doesn't support stepped increments")

@property
def color(self):
Expand All @@ -48,3 +48,6 @@ def _prevalidate_input(self, char):
shouldChangeCharactersInRange=NSRange(len(self.native.text), 0),
replacementString=char,
)

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
4 changes: 4 additions & 0 deletions iOS/tests_backend/widgets/textinput.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from rubicon.objc import SEL, send_message

from toga_iOS.libs import UITextField
Expand Down Expand Up @@ -56,3 +57,6 @@ def readonly(self):
def type_return(self):
# Invoke the return handler explicitly.
self.native.textFieldShouldReturn(self.native)

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
1 change: 1 addition & 0 deletions testbed/tests/widgets/test_multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
test_on_change_focus,
test_on_change_programmatic,
test_on_change_user,
test_undo_redo,
test_value_not_hidden,
)

Expand Down
38 changes: 38 additions & 0 deletions testbed/tests/widgets/test_numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import toga

from ..conftest import skip_on_platforms
from .properties import ( # noqa: F401
test_alignment,
test_background_color,
Expand Down Expand Up @@ -226,3 +227,40 @@ async def test_increment_decrement(widget, probe):
assert widget.value == expected
handler.assert_called_once_with(widget)
handler.reset_mock()


async def test_undo_redo(widget, probe):
"The widget supports undo and redo."
skip_on_platforms("android", "iOS", "linux", "windows")

widget.step = "0.00001"
text_0 = "3.14000"
text_1 = "3.14159"
text_extra = "159"
widget.value = text_0

widget.focus()
probe.set_cursor_at_end()

# type more text
for _ in text_extra:
await probe.type_character("<backspace>")
await probe.redraw(f"Widget value should be {text_0[:-3]!r}")
for char in text_extra:
await probe.type_character(char)
await probe.redraw(f"Widget value should be {text_1!r}")

assert widget.value == Decimal(text_1)
assert Decimal(probe.value) == Decimal(text_1)

# undo
await probe.undo()
await probe.redraw(f"Widget value should be {text_0!r}")
assert widget.value == Decimal(text_0)
assert Decimal(probe.value) == Decimal(text_0)

# redo
await probe.redo()
await probe.redraw(f"Widget value should be {text_1!r}")
assert widget.value == Decimal(text_1)
assert Decimal(probe.value) == Decimal(text_1)
1 change: 1 addition & 0 deletions testbed/tests/widgets/test_passwordinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
test_on_change_user,
test_on_confirm,
test_text_value,
test_undo_redo,
test_validation,
verify_focus_handlers,
verify_vertical_alignment,
Expand Down
30 changes: 30 additions & 0 deletions testbed/tests/widgets/test_textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,33 @@ async def test_text_value(widget, probe):

assert widget.value == str(text).replace("\n", " ")
assert probe.value == str(text).replace("\n", " ")


async def test_undo_redo(widget, probe):
"The widget supports undo and redo."

text_0 = str(widget.value)
text_extra = " World!"
text_1 = text_0 + text_extra

widget.focus()
probe.set_cursor_at_end()

# type more text
for char in text_extra:
await probe.type_character(char)
await probe.redraw(f"Widget value should be {text_1!r}")
assert widget.value == text_1
assert probe.value == text_1

# undo
await probe.undo()
await probe.redraw(f"Widget value should be {text_0!r}")
assert widget.value == text_0
assert probe.value == text_0

# redo
await probe.redo()
await probe.redraw(f"Widget value should be {text_1!r}")
assert widget.value == text_1
assert probe.value == text_1
7 changes: 7 additions & 0 deletions winforms/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from pytest import approx
from System import EventArgs, Object
from System.Drawing import Color, SystemColors
Expand Down Expand Up @@ -142,3 +143,9 @@ def is_hidden(self):
@property
def has_focus(self):
return self.native.ContainsFocus

async def undo(self):
pytest.skip("Undo not supported on this platform")

async def redo(self):
pytest.skip("Redo not supported on this platform")
4 changes: 4 additions & 0 deletions winforms/tests_backend/widgets/numberinput.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from System.Windows.Forms import NumericUpDown

from .base import SimpleProbe
Expand Down Expand Up @@ -38,3 +39,6 @@ def alignment(self):
def assert_vertical_alignment(self, expected):
# Vertical alignment isn't configurable in this native widget.
pass

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")
4 changes: 4 additions & 0 deletions winforms/tests_backend/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ctypes import c_uint
from ctypes.wintypes import HWND, LPARAM

import pytest
from System.Windows.Forms import TextBox

from .base import SimpleProbe
Expand Down Expand Up @@ -53,3 +54,6 @@ def alignment(self):
def assert_vertical_alignment(self, expected):
# Vertical alignment isn't configurable in this native widget.
pass

def set_cursor_at_end(self):
pytest.skip("Cursor positioning not supported on this platform")