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

[widget audit] toga.MultilineTextInput #1938

Merged
merged 43 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5cac536
Audit docs and core tests for MultilineTextInput.
freakboy3742 May 9, 2023
5429acc
WIP
freakboy3742 May 9, 2023
c0cf0f3
Cocoa MultilineTextView to 100% coverage.
freakboy3742 May 10, 2023
cc80700
iOS to 100% coverage.
freakboy3742 May 10, 2023
8fa3aa9
Add Changenote.
freakboy3742 May 10, 2023
9096f52
Use the UIKeyInput protocol to fake keyboard input.
freakboy3742 May 10, 2023
cf9da9c
Ensure a redraw includes at an event loop tick on macOS.
freakboy3742 May 11, 2023
5800c40
Spelling corrections in docstrings.
freakboy3742 May 11, 2023
6a5be8a
Tweaked some test docstrings.
freakboy3742 May 11, 2023
d89e49c
Gtk implementation to 100%
freakboy3742 May 15, 2023
2f8f214
Enable GTK focus tests.
freakboy3742 May 16, 2023
7aea2d0
Add a window manager to the GTK CI config.
freakboy3742 May 16, 2023
7fceda3
Correct iOS placeholder test definition.
freakboy3742 May 16, 2023
ead982d
Android WIP
mhsmith May 17, 2023
43759f9
Merge branch 'main' into audit-multilinetext
freakboy3742 May 17, 2023
c763aa9
Add type annotations.
freakboy3742 May 17, 2023
4cdab2c
Tweaked some docs formatting.
freakboy3742 May 18, 2023
ff27cde
Simplified some probe handling.
freakboy3742 May 18, 2023
e636a1f
Tweaked handling of on-change and enabled.
freakboy3742 May 18, 2023
98343dc
Correct GTK on_change handling.
freakboy3742 May 18, 2023
375d26c
Additional tolerance for scoll size.
freakboy3742 May 18, 2023
8ef583d
Another scrollbar tolerance tweak.
freakboy3742 May 18, 2023
838c6e4
Propegate widget descriptions to the API summary page.
freakboy3742 May 18, 2023
6dff342
Actually save all the changes before pushing...
freakboy3742 May 18, 2023
80ea9f6
Update Android for changes in testbed
mhsmith May 18, 2023
b3b8ed3
Add tests for vertical alignment, and fix on Android
mhsmith May 18, 2023
cbe63ec
Implement vertical alignment checks for GTK.
freakboy3742 May 19, 2023
9eec789
Removed and no-covered some unreachable and unused content in cocoa/i…
freakboy3742 May 19, 2023
2ff9920
Probe implementations (and widget implementation) for top vertical al…
freakboy3742 May 19, 2023
08c8d50
Added Winforms implementation and fix for vertical alignment.
freakboy3742 May 19, 2023
e379547
Fix Android background color
mhsmith May 20, 2023
5db4c77
Android at 100% coverage
mhsmith May 21, 2023
be9ca2f
Deprecated the clear() method on text inputs.
freakboy3742 May 22, 2023
07abb24
Add allowance for minor GTK style differences.
freakboy3742 May 23, 2023
e9132f0
Update change note and support table
mhsmith May 23, 2023
df00658
Make libs/android/graphics/drawable match Java package structure
mhsmith May 23, 2023
bb71950
All Winforms tests passing except test_scroll_position
mhsmith May 23, 2023
3ad5352
Winforms at 100% coverage
mhsmith May 23, 2023
e687898
Correct iOS test failure caused by focus.
freakboy3742 May 24, 2023
4ca8a9b
Document Winforms issue with TRANSPARENT backgrounds, and mainline th…
freakboy3742 May 24, 2023
87fffda
Correct GTK handling of clearing while focussed.
freakboy3742 May 24, 2023
fee4bb6
Simplify winforms implementation, removing proxy value.
freakboy3742 May 24, 2023
9f871ef
Winforms: fix interactions between placeholder and on_change handler
mhsmith May 25, 2023
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
18 changes: 15 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,21 @@ jobs:
- backend: linux
runs-on: ubuntu-22.04
# The package list should be the same as in tutorial-0.rst, and the BeeWare
# tutorial.
pre-command: "sudo apt-get update -y && sudo apt-get install -y python3-dev python3-cairo-dev python3-gi-cairo libgirepository1.0-dev libcairo2-dev libpango1.0-dev gir1.2-webkit2-4.0 pkg-config"
briefcase-run-prefix: 'xvfb-run -a -s "-screen 0 2048x1536x24"'
# tutorial, plus flwm to provide a window manager
pre-command: |
sudo apt-get update -y && sudo apt-get install -y python3-dev python3-cairo-dev python3-gi-cairo libgirepository1.0-dev libcairo2-dev libpango1.0-dev gir1.2-webkit2-4.0 pkg-config flwm

# Start Virtual X server
echo "Start X server..."
Xvfb :99 -screen 0 2048x1536x24 &
sleep 1

# Start Window manager
echo "Start window manager..."
DISPLAY=:99 flwm &
sleep 1

briefcase-run-prefix: 'DISPLAY=:99'
setup-python: false # Use the system Python packages.

- backend: windows
Expand Down
1 change: 1 addition & 0 deletions changes/1938.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The MultilineTextInput widget now has 100% test coverage, and complete API documentation.
3 changes: 3 additions & 0 deletions cocoa/src/toga_cocoa/libs/appkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,9 @@ class NSEventType(IntEnum):
RightMouseDragged = 7
MouseEntered = 8

KeyDown = 10
KeyUp = 11


######################################################################
# NSFont.h
Expand Down
68 changes: 45 additions & 23 deletions cocoa/src/toga_cocoa/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from travertino.size import at_least

from toga.colors import TRANSPARENT
from toga_cocoa.colors import native_color
from toga_cocoa.libs import (
NSBezelBorder,
NSScrollView,
NSTextAlignment,
NSTextView,
NSViewHeightSizable,
NSViewWidthSizable,
objc_method,
)
Expand All @@ -14,9 +17,8 @@

class TogaTextView(NSTextView):
@objc_method
def touchBar(self):
# Disable the touchbar.
return None
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was needed in the past to avoid a crash if the app ran on a MacBook with a Touch Bar; however, touchbars are no longer present on modern Macs, and in my testing on an older macBook with a Touch Bar, the bug no longer manifests.

def textDidChange_(self, notification) -> None:
self.interface.on_change(None)


class MultilineTextInput(Widget):
Expand All @@ -33,49 +35,69 @@ def create(self):
self.native.translatesAutoresizingMaskIntoConstraints = False

# Create the actual text widget
self.text = TogaTextView.alloc().init()
self.text.editable = True
self.text.selectable = True
self.text.verticallyResizable = True
self.text.horizontallyResizable = False
self.text.usesAdaptiveColorMappingForDarkAppearance = True
self.native_text = TogaTextView.alloc().init()
self.native_text.interface = self.interface
self.native_text.delegate = self.native_text

self.text.autoresizingMask = NSViewWidthSizable
self.native_text.editable = True
self.native_text.selectable = True
self.native_text.verticallyResizable = True
self.native_text.horizontallyResizable = False
self.native_text.usesAdaptiveColorMappingForDarkAppearance = True

self.native_text.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable

# Put the text view in the scroll window.
self.native.documentView = self.text
self.native.documentView = self.native_text

# Add the layout constraints
self.add_constraints()

def get_placeholder(self):
return self.native_text.placeholderString

def set_placeholder(self, value):
self.text.placeholderString = self.interface.placeholder
self.native_text.placeholderString = value

def get_readonly(self):
return not self.native_text.isEditable()

def set_readonly(self, value):
self.text.editable = not self.interface.readonly
self.native_text.editable = not value

def get_value(self):
return self.text.string
return self.native_text.string

def set_value(self, value):
self.text.string = value
self.native_text.string = value

def set_color(self, value):
self.text.textColor = native_color(value)
self.native_text.textColor = native_color(value)

def set_background_color(self, color):
if color is TRANSPARENT:
# Both the text view and the scroll view need to be made transparent
self.native.drawsBackground = False
self.native_text.drawsBackground = False
else:
# Both the text view and the scroll view need to be opaque,
# but only the text view needs a color.
self.native.drawsBackground = True
self.native_text.drawsBackground = True
self.native_text.backgroundColor = native_color(color)

def set_alignment(self, value):
self.native_text.alignment = NSTextAlignment(value)

def set_font(self, font):
if font:
self.text.font = font._impl.native
self.native_text.font = font._impl.native

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)

def set_on_change(self, handler):
self.interface.factory.not_implemented("MultilineTextInput.set_on_change()")

def scroll_to_bottom(self):
self.text.scrollToEndOfDocument(None)
self.native_text.scrollToEndOfDocument(None)

def scroll_to_top(self):
self.text.scrollToBeginningOfDocument(None)
self.native_text.scrollToBeginningOfDocument(None)
73 changes: 71 additions & 2 deletions cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import asyncio
from ctypes import c_void_p

from rubicon.objc import SEL, NSArray, NSObject, ObjCClass, objc_method
from rubicon.objc import SEL, NSArray, NSObject, NSPoint, ObjCClass, objc_method
from rubicon.objc.api import NSString

from toga.colors import TRANSPARENT
from toga.fonts import CURSIVE, FANTASY, MONOSPACE, SANS_SERIF, SERIF, SYSTEM
from toga_cocoa.libs import NSEvent, NSEventType
from toga_cocoa.libs.appkit import appkit

from .properties import toga_color
Expand Down Expand Up @@ -80,10 +81,14 @@ async def redraw(self, message=None):
# Force a repaint
self.widget.window.content._impl.native.displayIfNeeded()

# If we're running slow, wait for a second
if self.widget.app.run_slow:
# If we're running slow, wait for a second
print("Waiting for redraw" if message is None else message)
await asyncio.sleep(1)
else:
# Running at "normal" speed, we need to release to the event loop
# for at least one iteration. `runUntilDate:None` does this.
NSRunLoop.currentRunLoop.runUntilDate(None)

@property
def enabled(self):
Expand Down Expand Up @@ -141,3 +146,67 @@ def is_hidden(self):
@property
def has_focus(self):
return self.native.window.firstResponder == self.native

async def type_character(self, char):
# Convert the requested character into a Cocoa keycode.
# This table is incomplete, but covers all the basics.
key_code = {
" ": 49,
"\n": 36,
"a": 0,
"b": 11,
"c": 8,
"d": 2,
"e": 14,
"f": 3,
"g": 5,
"h": 4,
"i": 34,
"j": 38,
"k": 40,
"l": 37,
"m": 46,
"n": 45,
"o": 31,
"p": 35,
"q": 12,
"r": 15,
"s": 1,
"t": 17,
"u": 32,
"v": 9,
"w": 13,
"x": 7,
"y": 16,
"z": 6,
}.get(char.lower(), 0)

# 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,
timestamp=0,
windowNumber=self.native.window.windowNumber,
context=None,
characters=char,
charactersIgnoringModifiers=char,
isARepeat=False,
keyCode=key_code,
),
)
await self.post_event(
NSEvent.keyEventWithType(
NSEventType.KeyUp,
location=NSPoint(0, 0), # key presses don't have a location.
modifierFlags=0,
timestamp=0,
windowNumber=self.native.window.windowNumber,
context=None,
characters=char,
charactersIgnoringModifiers=char,
isARepeat=False,
keyCode=key_code,
),
)
88 changes: 87 additions & 1 deletion cocoa/tests_backend/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,97 @@
from toga.colors import TRANSPARENT
from toga_cocoa.libs import NSScrollView

from .base import SimpleProbe
from .properties import toga_alignment, toga_color, toga_font


class MultilineTextInputProbe(SimpleProbe):
native_class = NSScrollView

def __init__(self, widget):
super().__init__(widget)
self.native_text = widget._impl.native_text

@property
def value(self):
return str(self.native_text.string)

@property
def placeholder(self):
return str(self.native_text.placeholderString)

def placeholder_visible(self):
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
# macOS manages it's own placeholder visibility.
# We can use the existence of widget text as a proxy.
return not bool(self.native_text.string)

@property
def placeholder_hides_on_focus(self):
return False

@property
def color(self):
return toga_color(self.native_text.textColor)

@property
def background_color(self):
if self.native_text.drawsBackground:
# Confirm the scroll container is also opaque
assert self.native.drawsBackground
if self.native_text.backgroundColor:
return toga_color(self.native_text.backgroundColor)
else:
return None
else:
# Confirm the scroll container is also transparent
assert not self.native.drawsBackground
return TRANSPARENT

@property
def font(self):
return toga_font(self.native_text.font)

@property
def alignment(self):
return toga_alignment(self.native_text.alignment)

@property
def enabled(self):
# Enabled is proxied onto readonly on the text view
return self.native_text.isEditable()

@property
def readonly(self):
return not self.native.documentView.isEditable()
return not self.native_text.isEditable()

@property
def has_focus(self):
return self.native.window.firstResponder == self.native_text

@property
def visible_height(self):
return self.native.contentView.bounds.size.height

@property
def visible_width(self):
return self.native.contentView.bounds.size.width
mhsmith marked this conversation as resolved.
Show resolved Hide resolved

@property
def document_height(self):
return self.native_text.bounds.size.height

@property
def document_width(self):
return self.native_text.bounds.size.width

@property
def horizontal_scroll_position(self):
return self.native.contentView.bounds.origin.x

@property
def vertical_scroll_position(self):
return self.native.contentView.bounds.origin.y

async def wait_for_scroll_completion(self):
# No animation associated with scroll, so this is a no-op
pass
Loading