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

Two improvements to cocoa MultilineTextInput: disable richtext, and enable undo #2037

Closed
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
6 changes: 6 additions & 0 deletions android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,9 @@ def is_hidden(self):
@property
def has_focus(self):
return self.widget.app._impl.native.getCurrentFocus() == self.native

async def undo(self):
raise NotImplementedError()

async def redo(self):
raise NotImplementedError()
1 change: 1 addition & 0 deletions changes/2037.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The cocoa MultilineTextInput widget has been updated to disable richtext and enable undo
2 changes: 2 additions & 0 deletions cocoa/src/toga_cocoa/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def create(self):

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

from toga import Key
from toga.colors import TRANSPARENT
from toga_cocoa.keys import COCOA_MODIFIERS
from toga_cocoa.libs import NSEvent, NSEventType

from ..probe import BaseProbe
Expand Down Expand Up @@ -101,10 +103,11 @@ def is_hidden(self):
def has_focus(self):
return self.native.window.firstResponder == self.native

async def type_character(self, char):
async def type_character(self, char, modifiers=None):
# 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 @@ -136,12 +139,18 @@ async def type_character(self, char):
"z": 6,
}.get(char.lower(), 0)

modifier_flags = 0
if modifiers:
char = None # If `char` is a single character, the modifiers will be ignored. Only set keyCode.
for modifier in modifiers:
modifier_flags |= COCOA_MODIFIERS[modifier]

# This posts a single keyDown followed by a keyUp, matching "normal" keyboard operation.
await self.post_event(
NSEvent.keyEventWithType(
NSEventType.KeyDown,
location=NSPoint(0, 0), # key presses don't have a location.
modifierFlags=0,
modifierFlags=modifier_flags,
timestamp=0,
windowNumber=self.native.window.windowNumber,
context=None,
Expand All @@ -155,7 +164,7 @@ async def type_character(self, char):
NSEvent.keyEventWithType(
NSEventType.KeyUp,
location=NSPoint(0, 0), # key presses don't have a location.
modifierFlags=0,
modifierFlags=modifier_flags,
timestamp=0,
windowNumber=self.native.window.windowNumber,
context=None,
Expand All @@ -181,3 +190,9 @@ async def mouse_event(self, event_type, location, delay=None):
),
delay=delay,
)

async def undo(self):
await self.type_character("z", modifiers=[Key.MOD_1])

async def redo(self):
await self.type_character("z", modifiers=[Key.MOD_1, Key.SHIFT])
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, toga_font
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)
6 changes: 6 additions & 0 deletions gtk/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,9 @@ def event_handled(widget, e):

# Remove the temporary handler
self._keypress_target.disconnect(handler_id)

async def undo(self):
raise NotImplementedError()

async def redo(self):
raise NotImplementedError()
6 changes: 6 additions & 0 deletions iOS/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,9 @@ async def type_character(self, char):
self.native.insertText(char)
else:
self.native.insertText("")

async def undo(self):
raise NotImplementedError()

async def redo(self):
raise NotImplementedError()
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
37 changes: 37 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,39 @@ 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>")
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
32 changes: 32 additions & 0 deletions testbed/tests/widgets/test_textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import toga
from toga.constants import CENTER

from ..conftest import skip_on_platforms
from ..data import TEXTS
from .properties import ( # noqa: F401
test_alignment,
Expand Down Expand Up @@ -216,3 +217,34 @@ 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."
skip_on_platforms("android", "iOS", "linux", "windows")

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
6 changes: 6 additions & 0 deletions winforms/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,9 @@ def is_hidden(self):
@property
def has_focus(self):
return self.native.ContainsFocus

async def undo(self):
raise NotImplementedError()

async def redo(self):
raise NotImplementedError()