diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a727ee7a42..67e54a0b3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/android/src/toga_android/libs/android/graphics.py b/android/src/toga_android/libs/android/graphics/__init__.py similarity index 100% rename from android/src/toga_android/libs/android/graphics.py rename to android/src/toga_android/libs/android/graphics/__init__.py diff --git a/android/src/toga_android/libs/android/graphics/drawable.py b/android/src/toga_android/libs/android/graphics/drawable.py new file mode 100644 index 0000000000..1e907cb19d --- /dev/null +++ b/android/src/toga_android/libs/android/graphics/drawable.py @@ -0,0 +1,4 @@ +from rubicon.java import JavaClass + +ColorDrawable = JavaClass("android/graphics/drawable/ColorDrawable") +InsetDrawable = JavaClass("android/graphics/drawable/InsetDrawable") diff --git a/android/src/toga_android/widgets/base.py b/android/src/toga_android/widgets/base.py index aa9f1accfe..50e68f4e7f 100644 --- a/android/src/toga_android/widgets/base.py +++ b/android/src/toga_android/widgets/base.py @@ -4,6 +4,8 @@ from ..colors import native_color from ..libs.activity import MainActivity +from ..libs.android.graphics import PorterDuff__Mode, PorterDuffColorFilter, Rect +from ..libs.android.graphics.drawable import ColorDrawable, InsetDrawable from ..libs.android.view import Gravity, View @@ -109,18 +111,39 @@ def set_font(self, font): # By default, font can't be changed pass + # Although setBackgroundColor is defined in the View base class, we can't use it as + # a default implementation because it often overwrites other aspects of the widget's + # appearance. So each widget must decide how to implement this method, possibly + # using one of the utility functions below. def set_background_color(self, color): - # By default, background color can't be changed. pass - # Although setBackgroundColor is defined in the View base class, we can't use it as - # a default implementation because it often overwrites other aspects of the widget's - # appearance. - def set_background_color_simple(self, value): - if value is None: - self.native.setBackgroundColor(native_color(TRANSPARENT)) + def set_background_simple(self, value): + if not hasattr(self, "_default_background"): + self._default_background = self.native.getBackground() + + if value in (None, TRANSPARENT): + self.native.setBackground(self._default_background) else: - self.native.setBackgroundColor(native_color(value)) + background = ColorDrawable(native_color(value)) + if isinstance(self._default_background, InsetDrawable): + outer_padding = Rect() + inner_padding = Rect() + self._default_background.getPadding(outer_padding) + self._default_background.getDrawable().getPadding(inner_padding) + insets = [ + getattr(outer_padding, name) - getattr(inner_padding, name) + for name in ["left", "top", "right", "bottom"] + ] + background = InsetDrawable(background, *insets) + self.native.setBackground(background) + + def set_background_filter(self, value): + self.native.getBackground().setColorFilter( + None + if value in (None, TRANSPARENT) + else PorterDuffColorFilter(native_color(value), PorterDuff__Mode.SRC_IN) + ) def set_alignment(self, alignment): pass # If appropriate, a widget subclass will implement this. diff --git a/android/src/toga_android/widgets/box.py b/android/src/toga_android/widgets/box.py index d4f0ad112b..ddb3f15e84 100644 --- a/android/src/toga_android/widgets/box.py +++ b/android/src/toga_android/widgets/box.py @@ -20,7 +20,7 @@ def set_child_bounds(self, widget, x, y, width, height): self.native.updateViewLayout(widget.native, layout_params) def set_background_color(self, value): - self.set_background_color_simple(value) + self.set_background_simple(value) def rehint(self): self.interface.intrinsic.width = at_least(0) diff --git a/android/src/toga_android/widgets/button.py b/android/src/toga_android/widgets/button.py index def5e51d66..ed75072ed3 100644 --- a/android/src/toga_android/widgets/button.py +++ b/android/src/toga_android/widgets/button.py @@ -1,9 +1,5 @@ from travertino.size import at_least -from toga.colors import TRANSPARENT -from toga_android.colors import native_color - -from ..libs.android.graphics import PorterDuff__Mode, PorterDuffColorFilter from ..libs.android.view import OnClickListener, View__MeasureSpec from ..libs.android.widget import Button as A_Button from .label import TextViewWidget @@ -34,12 +30,7 @@ def set_enabled(self, value): self.native.setEnabled(value) def set_background_color(self, value): - # Do not use self.native.setBackgroundColor - this messes with the button style! - self.native.getBackground().setColorFilter( - None - if value is None or value == TRANSPARENT - else PorterDuffColorFilter(native_color(value), PorterDuff__Mode.SRC_IN) - ) + self.set_background_filter(value) def rehint(self): # Like other text-viewing widgets, Android crashes when rendering diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index 6e7ebe1831..cc3db34e26 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -25,6 +25,18 @@ def set_color(self, value): else: self.native.setTextColor(native_color(value)) + def set_textview_alignment(self, value, vertical_gravity): + # Justified text wasn't added until API level 26. + # We only run the test suite on API 31, so we need to disable branch coverage. + if Build.VERSION.SDK_INT >= 26: # pragma: no branch + self.native.setJustificationMode( + Layout.JUSTIFICATION_MODE_INTER_WORD + if value == JUSTIFY + else Layout.JUSTIFICATION_MODE_NONE + ) + + self.native.setGravity(vertical_gravity | align(value)) + class Label(TextViewWidget): def create(self): @@ -38,7 +50,7 @@ def set_text(self, value): self.native.setText(value) def set_background_color(self, value): - self.set_background_color_simple(value) + self.set_background_simple(value) def rehint(self): # Refuse to rehint an Android TextView if it has no LayoutParams yet. @@ -60,19 +72,4 @@ def rehint(self): self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) def set_alignment(self, value): - # Refuse to set alignment if widget has no container. - # On Android, calling setGravity() when the widget has no LayoutParams - # results in a NullPointerException. - if not self.native.getLayoutParams(): - return - - # Justified text wasn't added until API level 26. - # We only run the test suite on API 31, so we need to disable branch coverage. - if Build.VERSION.SDK_INT >= 26: # pragma: no branch - self.native.setJustificationMode( - Layout.JUSTIFICATION_MODE_INTER_WORD - if value == JUSTIFY - else Layout.JUSTIFICATION_MODE_NONE - ) - - self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) + self.set_textview_alignment(value, Gravity.TOP) diff --git a/android/src/toga_android/widgets/multilinetextinput.py b/android/src/toga_android/widgets/multilinetextinput.py index 7a71e58fd7..d7bba1d7fc 100644 --- a/android/src/toga_android/widgets/multilinetextinput.py +++ b/android/src/toga_android/widgets/multilinetextinput.py @@ -1,26 +1,21 @@ from travertino.size import at_least -from toga.constants import LEFT - from ..libs.android.text import InputType, TextWatcher from ..libs.android.view import Gravity from ..libs.android.widget import EditText -from .base import align from .label import TextViewWidget class TogaTextWatcher(TextWatcher): def __init__(self, impl): super().__init__() - self.impl = impl self.interface = impl.interface def beforeTextChanged(self, _charSequence, _start, _count, _after): pass def afterTextChanged(self, _editable): - if self.interface.on_change: - self.interface.on_change(widget=self.interface) + self.interface.on_change(None) def onTextChanged(self, _charSequence, _start, _before, _count): pass @@ -28,44 +23,50 @@ def onTextChanged(self, _charSequence, _start, _before, _count): class MultilineTextInput(TextViewWidget): def create(self): - self._textChangedListener = None self.native = EditText(self._native_activity) self.native.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE ) - # Set default alignment - self.set_alignment(LEFT) + self.native.addTextChangedListener(TogaTextWatcher(self)) self.cache_textview_defaults() def get_value(self): - return self.native.getText().toString() + return str(self.native.getText()) + + def set_value(self, value): + self.native.setText(value) + + def get_readonly(self): + return not self.native.isFocusable() - def set_readonly(self, value): - self.native.setFocusable(not value) + def set_readonly(self, readonly): + if readonly: + # Implicitly calls setFocusableInTouchMode(False) + self.native.setFocusable(False) + else: + # Implicitly calls setFocusable(True) + self.native.setFocusableInTouchMode(True) + + def get_placeholder(self): + return str(self.native.getHint()) def set_placeholder(self, value): - # Android EditText's setHint() requires a Python string. - self.native.setHint(value if value is not None else "") + self.native.setHint(value) def set_alignment(self, value): - self.native.setGravity(Gravity.TOP | align(value)) - - def set_value(self, value): - self.native.setText(value) + self.set_textview_alignment(value, Gravity.TOP) - def set_on_change(self, handler): - if self._textChangedListener: - self.native.removeTextChangedListener(self._textChangedListener) - self._textChangedListener = TogaTextWatcher(self) - self.native.addTextChangedListener(self._textChangedListener) + def set_background_color(self, value): + # This causes any custom color to hide the bottom border line, but it's better + # than set_background_filter, which affects *only* the bottom border line. + self.set_background_simple(value) def rehint(self): self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH) self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT) def scroll_to_bottom(self): - last_line = (self.native.getLineCount() - 1) * self.native.getLineHeight() - self.native.scrollTo(0, last_line) + self.native.setSelection(self.native.length()) def scroll_to_top(self): - self.native.scrollTo(0, 0) + self.native.setSelection(0) diff --git a/android/src/toga_android/widgets/textinput.py b/android/src/toga_android/widgets/textinput.py index b5d2684107..ed07e71799 100644 --- a/android/src/toga_android/widgets/textinput.py +++ b/android/src/toga_android/widgets/textinput.py @@ -5,7 +5,6 @@ from ..libs.android.text import InputType, TextWatcher from ..libs.android.view import Gravity, OnKeyListener, View__MeasureSpec from ..libs.android.widget import EditText -from .base import align from .label import TextViewWidget @@ -64,12 +63,7 @@ def set_placeholder(self, value): self.native.setHint(value if value is not None else "") def set_alignment(self, value): - # Refuse to set alignment unless widget has been added to a container. - # This is because Android EditText requires LayoutParams before - # setGravity() can be called. - if not self.native.getLayoutParams(): - return - self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) + self.set_textview_alignment(value, Gravity.CENTER_VERTICAL) def set_value(self, value): self.native.setText(value) diff --git a/android/src/toga_android/widgets/webview.py b/android/src/toga_android/widgets/webview.py index c33b9a59a1..04f568502e 100644 --- a/android/src/toga_android/widgets/webview.py +++ b/android/src/toga_android/widgets/webview.py @@ -3,9 +3,9 @@ from travertino.size import at_least -from ..libs.android.view import Gravity, View__MeasureSpec +from ..libs.android.view import View__MeasureSpec from ..libs.android.webkit import ValueCallback, WebView as A_WebView, WebViewClient -from .base import Widget, align +from .base import Widget class ReceiveString(ValueCallback): @@ -72,13 +72,6 @@ async def evaluate_javascript(self, javascript): def invoke_javascript(self, javascript): self.native.evaluateJavascript(str(javascript), ReceiveString()) - def set_alignment(self, value): - # Refuse to set alignment unless widget has been added to a container. - # This is because this widget's setGravity() requires LayoutParams before it can be called. - if not self.native.getLayoutParams(): - return - self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) - def rehint(self): self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH) # Refuse to call measure() if widget has no container, i.e., has no LayoutParams. diff --git a/android/tests_backend/widgets/base.py b/android/tests_backend/widgets/base.py index fb26016de7..765dfd8b13 100644 --- a/android/tests_backend/widgets/base.py +++ b/android/tests_backend/widgets/base.py @@ -3,8 +3,15 @@ from java import dynamic_proxy from pytest import approx +from android.graphics.drawable import ( + ColorDrawable, + DrawableContainer, + DrawableWrapper, + LayerDrawable, +) from android.os import Build from android.view import View, ViewTreeObserver +from toga.colors import TRANSPARENT from toga.fonts import SYSTEM from toga.style.pack import JUSTIFY, LEFT @@ -122,7 +129,34 @@ def assert_layout(self, size, position): @property def background_color(self): - return toga_color(self.native.getBackground().getColor()) + background = self.native.getBackground() + while True: + if isinstance(background, ColorDrawable): + return toga_color(background.getColor()) + + # The following complex Drawables all apply color filters to their children, + # but they don't implement getColorFilter, at least not in our current + # minimum API level. + elif isinstance(background, LayerDrawable): + background = background.getDrawable(0) + elif isinstance(background, DrawableContainer): + background = background.getCurrent() + elif isinstance(background, DrawableWrapper): + background = background.getDrawable() + + else: + break + + if background is None: + return TRANSPARENT + filter = background.getColorFilter() + if filter: + # PorterDuffColorFilter.getColor is undocumented, but continues to work for + # now. If this method is blocked in the future, another option is to use the + # filter to draw something and see what color comes out. + return toga_color(filter.getColor()) + else: + return TRANSPARENT async def press(self): self.native.performClick() diff --git a/android/tests_backend/widgets/button.py b/android/tests_backend/widgets/button.py index 2475262e8a..6cdd2fecc5 100644 --- a/android/tests_backend/widgets/button.py +++ b/android/tests_backend/widgets/button.py @@ -1,10 +1,9 @@ from java import jclass -from android.graphics.drawable import DrawableWrapper, LayerDrawable +from toga.colors import TRANSPARENT from toga.fonts import SYSTEM from .label import LabelProbe -from .properties import toga_color # On Android, a Button is just a TextView with a state-dependent background image. @@ -20,24 +19,5 @@ def assert_font_family(self, expected): @property def background_color(self): - background = self.native.getBackground() - while True: - if isinstance(background, LayerDrawable): - # LayerDrawable applies color filters to all of its layers, but it doesn't - # implement getColorFilter itself. - background = background.getDrawable(0) - elif isinstance(background, DrawableWrapper): - # DrawableWrapper doesn't implement getColorFilter in API level 24, but - # has implemented it by API level 33. - background = background.getDrawable() - else: - break - - filter = background.getColorFilter() - if filter: - # PorterDuffColorFilter.getColor is undocumented, but continues to work for - # now. If this method is blocked in the future, another option is to use the - # filter to draw something and see what color comes out. - return toga_color(filter.getColor()) - else: - return None + color = super().background_color + return None if color == TRANSPARENT else color diff --git a/android/tests_backend/widgets/label.py b/android/tests_backend/widgets/label.py index 5f322a4f19..56dfaa42a3 100644 --- a/android/tests_backend/widgets/label.py +++ b/android/tests_backend/widgets/label.py @@ -3,7 +3,7 @@ from android.os import Build from .base import SimpleProbe -from .properties import toga_alignment, toga_color, toga_font +from .properties import toga_alignment, toga_color, toga_font, toga_vertical_alignment class LabelProbe(SimpleProbe): @@ -31,3 +31,7 @@ def alignment(self): None if Build.VERSION.SDK_INT < 26 else self.native.getJustificationMode() ) return toga_alignment(self.native.getGravity(), justification_mode) + + @property + def vertical_alignment(self): + return toga_vertical_alignment(self.native.getGravity()) diff --git a/android/tests_backend/widgets/multilinetextinput.py b/android/tests_backend/widgets/multilinetextinput.py new file mode 100644 index 0000000000..297ce90e32 --- /dev/null +++ b/android/tests_backend/widgets/multilinetextinput.py @@ -0,0 +1,45 @@ +from java import jclass + +from .label import LabelProbe + + +class MultilineTextInputProbe(LabelProbe): + native_class = jclass("android.widget.EditText") + + @property + def value(self): + return self.native.getHint() if self.placeholder_visible else self.text + + @property + def placeholder_visible(self): + return not self.text + + @property + def placeholder_hides_on_focus(self): + return False + + @property + def readonly(self): + focusable = self.native.isFocusable() + focusable_in_touch_mode = self.native.isFocusableInTouchMode() + if focusable != focusable_in_touch_mode: + raise ValueError(f"invalid state: {focusable=}, {focusable_in_touch_mode=}") + return not focusable + + @property + def document_height(self): + return self.native.getLayout().getHeight() / self.scale_factor + + @property + def document_width(self): + return self.native.getLayout().getWidth() / self.scale_factor + + @property + def vertical_scroll_position(self): + return self.native.getScrollY() / self.scale_factor + + async def wait_for_scroll_completion(self): + pass + + async def type_character(self, char): + self.native.append(char) diff --git a/android/tests_backend/widgets/properties.py b/android/tests_backend/widgets/properties.py index 60cd78ee81..76d1d18575 100644 --- a/android/tests_backend/widgets/properties.py +++ b/android/tests_backend/widgets/properties.py @@ -7,7 +7,7 @@ from android.util import TypedValue from android.view import Gravity from toga.colors import TRANSPARENT, rgba -from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT +from toga.constants import BOTTOM, CENTER, JUSTIFY, LEFT, RIGHT, TOP from toga.fonts import ( BOLD, ITALIC, @@ -86,3 +86,12 @@ def toga_alignment(gravity, justification_mode): return JUSTIFY else: raise ValueError(f"unknown combination: {gravity=}, {justification_mode=}") + + +def toga_vertical_alignment(gravity): + vertical_gravity = gravity & Gravity.VERTICAL_GRAVITY_MASK + return { + Gravity.TOP: TOP, + Gravity.BOTTOM: BOTTOM, + Gravity.CENTER_VERTICAL: CENTER, + }[vertical_gravity] diff --git a/changes/1938.feature.rst b/changes/1938.feature.rst new file mode 100644 index 0000000000..1b4b1de04d --- /dev/null +++ b/changes/1938.feature.rst @@ -0,0 +1 @@ +The MultilineTextInput widget now has 100% test coverage, and complete API documentation. diff --git a/changes/1938.removal.rst b/changes/1938.removal.rst new file mode 100644 index 0000000000..6460bbbea3 --- /dev/null +++ b/changes/1938.removal.rst @@ -0,0 +1 @@ +The ``clear()`` method for resetting the value of a MultilineTextInput, TextInput and PasswordInput has been removed. This method was an ambiguous override of the ``clear()`` method on Widget that removed all child nodes. To remove all content from a text input widget, use ``widget.value = ""``. diff --git a/cocoa/src/toga_cocoa/libs/appkit.py b/cocoa/src/toga_cocoa/libs/appkit.py index e381c52b6d..53910850ef 100644 --- a/cocoa/src/toga_cocoa/libs/appkit.py +++ b/cocoa/src/toga_cocoa/libs/appkit.py @@ -2,28 +2,12 @@ # System/Library/Frameworks/AppKit.framework ########################################################################## import platform -from ctypes import Structure, c_void_p +from ctypes import c_void_p from enum import Enum, IntEnum -from rubicon.objc import CGFloat, ObjCClass, objc_const +from rubicon.objc import ObjCClass, objc_const from rubicon.objc.api import NSString from rubicon.objc.runtime import load_library -from travertino.colors import ( - BLACK, - BLUE, - BROWN, - CYAN, - DARKGRAY, - GRAY, - GREEN, - LIGHTGRAY, - MAGENTA, - ORANGE, - PURPLE, - RED, - WHITE, - YELLOW, -) from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT @@ -87,7 +71,7 @@ class NSAlertStyle(Enum): NSAboutPanelOptionVersion = NSString( c_void_p.in_dll(appkit, "NSAboutPanelOptionVersion") ) -except ValueError: +except ValueError: # pragma: no cover NSAboutPanelOptionApplicationIcon = None NSAboutPanelOptionApplicationName = None NSAboutPanelOptionApplicationVersion = None @@ -292,25 +276,6 @@ class NSBezelStyle(IntEnum): NSColor.declare_class_property("yellowColor") -def NSColorUsingColorName(background_color): - return { - BLACK: NSColor.blackColor, - BLUE: NSColor.blueColor, - BROWN: NSColor.brownColor, - CYAN: NSColor.cyanColor, - DARKGRAY: NSColor.darkGrayColor, - GRAY: NSColor.grayColor, - GREEN: NSColor.greenColor, - LIGHTGRAY: NSColor.lightGrayColor, - MAGENTA: NSColor.magentaColor, - ORANGE: NSColor.orangeColor, - PURPLE: NSColor.purpleColor, - RED: NSColor.redColor, - WHITE: NSColor.whiteColor, - YELLOW: NSColor.yellowColor, - }[background_color] - - ###################################################################### # NSCursor.h @@ -379,6 +344,9 @@ class NSEventType(IntEnum): RightMouseDragged = 7 MouseEntered = 8 + KeyDown = 10 + KeyUp = 11 + ###################################################################### # NSFont.h @@ -440,10 +408,10 @@ class NSImageAlignment(Enum): NSImageScaleNone = 2 NSImageScaleProportionallyUpOrDown = 3 -if platform.machine() == "arm64": +if platform.machine() == "arm64": # pragma: no cover NSImageResizingModeTile = 1 NSImageResizingModeStretch = 0 -else: +else: # pragma: no cover NSImageResizingModeTile = 0 NSImageResizingModeStretch = 1 @@ -513,19 +481,6 @@ class NSImageResizingMode(Enum): # NSLayoutConstraintOrientationVertical = 1 -class NSEdgeInsets(Structure): - _fields_ = [ - ("top", CGFloat), - ("left", CGFloat), - ("bottom", CGFloat), - ("right", CGFloat), - ] - - -def NSEdgeInsetsMake(top, left, bottom, right): - return NSEdgeInsets(top, left, bottom, right) - - class NSLayoutPriority(Enum): Required = 1000 DefaultHigh = 750 @@ -716,10 +671,10 @@ class NSTableViewAnimation(Enum): ###################################################################### # NSText.h NSLeftTextAlignment = 0 -if platform.machine() == "arm64": +if platform.machine() == "arm64": # pragma: no cover NSRightTextAlignment = 2 NSCenterTextAlignment = 1 -else: +else: # pragma: no cover NSRightTextAlignment = 1 NSCenterTextAlignment = 2 diff --git a/cocoa/src/toga_cocoa/widgets/multilinetextinput.py b/cocoa/src/toga_cocoa/widgets/multilinetextinput.py index 010fc4527d..b4d59e26cd 100644 --- a/cocoa/src/toga_cocoa/widgets/multilinetextinput.py +++ b/cocoa/src/toga_cocoa/widgets/multilinetextinput.py @@ -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, ) @@ -14,9 +17,8 @@ class TogaTextView(NSTextView): @objc_method - def touchBar(self): - # Disable the touchbar. - return None + def textDidChange_(self, notification) -> None: + self.interface.on_change(None) class MultilineTextInput(Widget): @@ -33,49 +35,77 @@ 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_enabled(self): + return self.native_text.isSelectable() + + def set_enabled(self, value): + self.native_text.editable = value + self.native_text.selectable = 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 + self.interface.on_change(None) 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) diff --git a/cocoa/tests_backend/widgets/base.py b/cocoa/tests_backend/widgets/base.py index 69155ebb67..58848dc784 100644 --- a/cocoa/tests_backend/widgets/base.py +++ b/cocoa/tests_backend/widgets/base.py @@ -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 @@ -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): @@ -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, + ), + ) diff --git a/cocoa/tests_backend/widgets/label.py b/cocoa/tests_backend/widgets/label.py index d4faa7e066..30cf5d1ac0 100644 --- a/cocoa/tests_backend/widgets/label.py +++ b/cocoa/tests_backend/widgets/label.py @@ -1,3 +1,4 @@ +from toga.constants import TOP from toga_cocoa.libs import NSTextField from .base import SimpleProbe @@ -22,3 +23,9 @@ def font(self): @property def alignment(self): return toga_alignment(self.native.alignment) + + @property + def vertical_alignment(self): + # Cocoa doesn't provide an option to alter the vertical alignment of + # NSTextField + return TOP diff --git a/cocoa/tests_backend/widgets/multilinetextinput.py b/cocoa/tests_backend/widgets/multilinetextinput.py index c3c618c4e1..901e56a9d2 100644 --- a/cocoa/tests_backend/widgets/multilinetextinput.py +++ b/cocoa/tests_backend/widgets/multilinetextinput.py @@ -1,11 +1,96 @@ +from toga.colors import TRANSPARENT +from toga.constants import TOP 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.placeholderString + if self.placeholder_visible + else self.native_text.string + ) + + @property + def placeholder_visible(self): + # 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 vertical_alignment(self): + # Cocoa doesn't provide an option to alter the vertical alignment of + # NSTextView + return TOP + + @property + def enabled(self): + return self.native_text.isSelectable() + @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 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 diff --git a/core/src/toga/widgets/canvas.py b/core/src/toga/widgets/canvas.py index 145e2e5e14..47d2cc9a22 100644 --- a/core/src/toga/widgets/canvas.py +++ b/core/src/toga/widgets/canvas.py @@ -96,11 +96,13 @@ def remove(self, drawing_object): Args: drawing_object (:obj:'Drawing Object'): The drawing object to remove """ + # AUDIT NOTE: Should this be removed? It overrides Widget.remove() self.drawing_objects.remove(drawing_object) self.redraw() def clear(self): """Remove all drawing objects.""" + # AUDIT NOTE: Should this be removed? It overrides Widget.clear() self.drawing_objects.clear() self.redraw() diff --git a/core/src/toga/widgets/multilinetextinput.py b/core/src/toga/widgets/multilinetextinput.py index 292bc876e4..0801419093 100644 --- a/core/src/toga/widgets/multilinetextinput.py +++ b/core/src/toga/widgets/multilinetextinput.py @@ -1,148 +1,103 @@ -import warnings - from toga.handlers import wrapped_handler from .base import Widget class MultilineTextInput(Widget): - """A multi-line text input widget. - - Args: - id (str): An identifier for this widget. - style(:obj:`Style`): An optional style object. - If no style is provided then a new one will be created for the widget. - value (str): The initial text of the widget. - readonly (bool): Whether a user can write into the text input, - defaults to `False`. - placeholder (str): The placeholder text for the widget. - on_change (``callable``): The handler to invoke when the text changes. - """ - - MIN_HEIGHT = 100 - MIN_WIDTH = 100 - def __init__( self, id=None, style=None, - factory=None, # DEPRECATED! - value=None, - readonly=False, - placeholder=None, + value: str = None, + readonly: bool = False, + placeholder: str = None, on_change=None, - initial=None, # DEPRECATED! ): - super().__init__(id=id, style=style) + """Create a new multi-line text input widget. + + Inherits from :class:`~toga.widgets.base.Widget`. + + :param id: The ID for the widget. + :param style: A style object. If no style is provided, a default style + will be applied to the widget. + :param value: The initial content to display in the widget. + :param readonly: Can the value of the widget be modified by the user? + :param placeholder: The content to display as a placeholder when + there is no user content to display. + :param on_change: A handler that will be invoked when the the value of + the widget changes. + """ - ###################################################################### - # 2022-09: Backwards compatibility - ###################################################################### - # factory no longer used - if factory: - warnings.warn("The factory argument is no longer used.", DeprecationWarning) - ###################################################################### - # End backwards compatibility. - ###################################################################### + super().__init__(id=id, style=style) # Create a platform specific implementation of a MultilineTextInput self._impl = self.factory.MultilineTextInput(interface=self) - ################################################################## - # 2022-07: Backwards compatibility - ################################################################## - - # initial replaced with value - if initial is not None: - if value is not None: - raise ValueError( - "Cannot specify both `initial` and `value`; " - "`initial` has been deprecated, use `value`" - ) - else: - warnings.warn("`initial` has been renamed `value`", DeprecationWarning) - value = initial - - ################################################################## - # End backwards compatibility. - ################################################################## + # Set a dummy handler before installing the actual on_change, because we do not want + # on_change triggered by the initial value being set + self.on_change = None + self.value = value # Set all the properties - self.value = value self.readonly = readonly self.placeholder = placeholder self.on_change = on_change @property - def placeholder(self): - """The placeholder text. + def placeholder(self) -> str: + """The placeholder text for the widget. - Returns: - The placeholder text as a `str``. + A value of ``None`` will be interpreted and returned as an empty string. + Any other object will be converted to a string using ``str()``. """ - return self._placeholder + return self._impl.get_placeholder() @placeholder.setter def placeholder(self, value): - self._placeholder = "" if value is None else str(value) - self._impl.set_placeholder(self._placeholder) + self._impl.set_placeholder("" if value is None else str(value)) + self.refresh() @property - def readonly(self): - """Whether a user can write into the text input. + def readonly(self) -> bool: + """Can the value of the widget be modified by the user? - Returns: - `True` if the user can only read, `False` if the user can read and write the text. + This only controls manual changes by the user (i.e., typing at the + keyboard). Programmatic changes are permitted while the widget has + ``readonly`` enabled. """ - return self._readonly + return self._impl.get_readonly() @readonly.setter def readonly(self, value): - self._readonly = value - self._impl.set_readonly(value) + self._impl.set_readonly(bool(value)) @property - def value(self): - """The value of the multi line text input field. + def value(self) -> str: + """The text to display in the widget. - Returns: - The text of the Widget as a ``str``. + A value of ``None`` will be interpreted and returned as an empty string. + Any other object will be converted to a string using ``str()``. """ return self._impl.get_value() @value.setter def value(self, value): - cleaned_value = "" if value is None else str(value) - self._impl.set_value(cleaned_value) + self._impl.set_value("" if value is None else str(value)) self.refresh() - def clear(self): - """Clears the text from the widget.""" - self.value = "" + def scroll_to_bottom(self): + """Scroll the view to make the bottom of the text field visible.""" + self._impl.scroll_to_bottom() + + def scroll_to_top(self): + """Scroll the view to make the top of the text field visible.""" + self._impl.scroll_to_top() @property def on_change(self): - """The handler to invoke when the value changes. - - Returns: - The function ``callable`` that is called on a content change. - """ + """The handler to invoke when the value of the widget changes.""" return self._on_change @on_change.setter def on_change(self, handler): - """Set the handler to invoke when the value is changed. - - Args: - handler (:obj:`callable`): The handler to invoke when the value is changed. - """ self._on_change = wrapped_handler(self, handler) - self._impl.set_on_change(self._on_change) - - def scroll_to_bottom(self): - """Scroll the view to make the bottom of the text field visible.""" - self._impl.scroll_to_bottom() - - def scroll_to_top(self): - """Scroll the view to make the top of the text field visible.""" - self._impl.scroll_to_top() diff --git a/core/src/toga/widgets/textinput.py b/core/src/toga/widgets/textinput.py index f1bc0270b0..aab1e6448b 100644 --- a/core/src/toga/widgets/textinput.py +++ b/core/src/toga/widgets/textinput.py @@ -147,10 +147,6 @@ def value(self, value): def is_valid(self): return self._impl.is_valid() - def clear(self): - """Clears the text of the widget.""" - self.value = "" - @property def on_change(self): """The handler to invoke when the value changes. diff --git a/core/tests/test_deprecated_factory.py b/core/tests/test_deprecated_factory.py index 3b35d6acb5..04893941d7 100644 --- a/core/tests/test_deprecated_factory.py +++ b/core/tests/test_deprecated_factory.py @@ -100,12 +100,6 @@ def test_image_view_created(self): self.assertEqual(widget._impl.interface, widget) self.assertNotEqual(widget.factory, self.factory) - def test_multiline_text_input_created(self): - with self.assertWarns(DeprecationWarning): - widget = toga.MultilineTextInput(factory=self.factory) - self.assertEqual(widget._impl.interface, widget) - self.assertNotEqual(widget.factory, self.factory) - def test_number_input_created(self): with self.assertWarns(DeprecationWarning): widget = toga.NumberInput(factory=self.factory) diff --git a/core/tests/widgets/test_multilinetextinput.py b/core/tests/widgets/test_multilinetextinput.py index 2bed71f3e0..efb8bbccab 100644 --- a/core/tests/widgets/test_multilinetextinput.py +++ b/core/tests/widgets/test_multilinetextinput.py @@ -1,58 +1,164 @@ -from unittest import mock +from unittest.mock import Mock + +import pytest import toga -from toga_dummy.utils import TestCase - - -class MultilineTextInputTests(TestCase): - def setUp(self): - super().setUp() - - self.value = "Super Multiline Text" - self.on_change = mock.Mock() - self.multiline = toga.MultilineTextInput( - value=self.value, - on_change=self.on_change, - ) - - def test_widget_created(self): - self.assertEqual(self.multiline._impl.interface, self.multiline) - self.assertActionPerformed(self.multiline, "create MultilineTextInput") - self.assertEqual(self.multiline.on_change._raw, self.on_change) - - def test_multiline_properties_with_None(self): - self.assertEqual(self.multiline.readonly, False) - self.assertEqual(self.multiline.value, self.value) - self.assertEqual(self.multiline.placeholder, "") - - def test_multiline_values(self): - new_value = "New Multiline Text" - self.multiline.value = new_value - self.assertEqual(self.multiline.value, new_value) - self.multiline.clear() - self.assertEqual(self.multiline.value, "") - - ###################################################################### - # 2022-07: Backwards compatibility - ###################################################################### - - def test_init_with_deprecated(self): - # initial is a deprecated argument - with self.assertWarns(DeprecationWarning): - my_text_input = toga.MultilineTextInput( - initial=self.value, - on_change=self.on_change, - ) - self.assertEqual(my_text_input.value, self.value) - - # can't specify both initial *and* value - with self.assertRaises(ValueError): - toga.MultilineTextInput( - initial=self.value, - value=self.value, - on_change=self.on_change, - ) - - ###################################################################### - # End backwards compatibility. - ###################################################################### +from toga_dummy.utils import EventLog, assert_action_performed, attribute_value + + +@pytest.fixture +def widget(): + return toga.MultilineTextInput() + + +def test_widget_created(widget): + "A multiline text input" + assert widget._impl.interface == widget + assert_action_performed(widget, "create MultilineTextInput") + + assert not widget.readonly + assert widget.placeholder == "" + assert widget.value == "" + assert widget._on_change._raw is None + + +def test_create_with_values(): + "A multiline text input can be created with initial values" + on_change = Mock() + widget = toga.MultilineTextInput( + value="Some text", + placeholder="A placeholder", + readonly=True, + on_change=on_change, + ) + assert widget._impl.interface == widget + assert_action_performed(widget, "create MultilineTextInput") + + assert widget.readonly + assert widget.placeholder == "A placeholder" + assert widget.value == "Some text" + assert widget._on_change._raw == on_change + + +@pytest.mark.parametrize( + "value, expected", + [ + ("New Text", "New Text"), + ("", ""), + (None, ""), + (12345, "12345"), + ("Contains\nnewline", "Contains\nnewline"), + ], +) +def test_value(widget, value, expected): + """The value of the input can be set.""" + # Clear the event log + EventLog.reset() + + # Install an on_change handler + handler = Mock() + widget.on_change = handler + + widget.value = value + assert widget.value == expected + + # test backend has the right value + assert attribute_value(widget, "value") == expected + + # A refresh was performed + assert_action_performed(widget, "refresh") + + # Callback was invoked + handler.assert_called_once_with(widget) + + +@pytest.mark.parametrize( + "value, expected", + [ + (None, False), + ("", False), + ("true", True), + ("false", True), # Evaluated as a string, this value is true. + (0, False), + (1234, True), + ], +) +def test_readonly(widget, value, expected): + "The readonly status of the widget can be changed." + # Widget is initially not readonly by default. + assert not widget.readonly + + # Set the readonly status + widget.readonly = value + assert widget.readonly == expected + + # Set the widget readonly + widget.readonly = True + assert widget.readonly + + # Set the readonly status again + widget.readonly = value + assert widget.readonly == expected + + +@pytest.mark.parametrize( + "value, expected", + [ + ("New Text", "New Text"), + ("", ""), + (None, ""), + (12345, "12345"), + ("Contains\nnewline", "Contains\nnewline"), + ], +) +def test_placeholder(widget, value, expected): + """The value of the placeholder can be set.""" + # Clear the event log + EventLog.reset() + + widget.placeholder = value + assert widget.placeholder == expected + + # test backend has the right value + assert attribute_value(widget, "placeholder") == expected + + # A refresh was performed + assert_action_performed(widget, "refresh") + + +def test_scroll(widget): + """The widget can be scrolled programatically.""" + # Clear the event log + EventLog.reset() + + widget.scroll_to_top() + + # A refresh was performed + assert_action_performed(widget, "scroll to top") + + # Clear the event log + EventLog.reset() + + widget.scroll_to_bottom() + + # The widget has been scrolled + assert_action_performed(widget, "scroll to bottom") + + +def test_on_change(widget): + """The on_change handler can be invoked.""" + # No handler initially + assert widget._on_change._raw is None + + # Define and set a new callback + handler = Mock() + + widget.on_change = handler + + assert widget.on_change._raw == handler + + # Invoke the callback + widget._impl.simulate_change() + + # Callback was invoked + handler.assert_called_once_with(widget) diff --git a/core/tests/widgets/test_passwordinput.py b/core/tests/widgets/test_passwordinput.py index 6255c17820..09642547b9 100644 --- a/core/tests/widgets/test_passwordinput.py +++ b/core/tests/widgets/test_passwordinput.py @@ -29,9 +29,6 @@ def test_widget(self): self.password_input.value = new_value self.assertEqual(self.password_input.value, new_value) - self.password_input.clear() - self.assertEqual(self.password_input.value, "") - def test_focus(self): self.password_input.focus() self.assertActionPerformed(self.password_input, "focus") diff --git a/core/tests/widgets/test_textinput.py b/core/tests/widgets/test_textinput.py index e37bf5bcdd..ebb2ab740b 100644 --- a/core/tests/widgets/test_textinput.py +++ b/core/tests/widgets/test_textinput.py @@ -30,10 +30,6 @@ def test_arguments_are_all_set_properly(self): self.assertEqual(self.text_input.placeholder, self.placeholder) self.assertEqual(self.text_input.readonly, self.readonly) - def test_clear(self): - self.text_input.clear() - self.assertValueSet(self.text_input, "value", "") - def test_set_placeholder_with_None(self): self.text_input.placeholder = None self.assertEqual(self.text_input.placeholder, "") diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index a60cf99047..8e0fec9e03 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -18,41 +18,48 @@ Core application components General widgets --------------- -======================================================================= ==================================== +======================================================================= ======================================================================== Component Description -======================================================================= ==================================== - :doc:`ActivityIndicator ` A (spinning) activity indicator - :doc:`Button ` Basic clickable Button +======================================================================= ======================================================================== + :doc:`ActivityIndicator ` A small animated indicator showing activity on a task of indeterminate + length, usually rendered as a "spinner" animation. + :doc:`Button ` A button that can be pressed or clicked. :doc:`Canvas ` Area you can draw on :doc:`DetailedList ` A list of complex content - :doc:`Divider ` A horizontal or vertical line + :doc:`Divider ` A separator used to visually distinguish two sections of content in a + layout. :doc:`ImageView ` Image Viewer - :doc:`Label ` Text label - :doc:`MultilineTextInput ` Multi-line Text Input field + :doc:`Label ` A text label for annotating forms or interfaces. + :doc:`MultilineTextInput ` A scrollable panel that allows for the display and editing of multiple + lines of text. :doc:`NumberInput ` Number Input field :doc:`PasswordInput ` A text input that hides it's input - :doc:`ProgressBar ` Progress Bar + :doc:`ProgressBar ` A horizontal bar to visualize task progress. The task being monitored + can be of known or indeterminate length. :doc:`Selection ` Selection - :doc:`Slider ` Slider - :doc:`Switch ` Switch + :doc:`Slider ` A widget for selecting a value within a range. The range is shown as a + horizontal line, and the selected value is shown as a draggable marker. + :doc:`Switch ` A clickable button with two stable states: True (on, checked); and + False (off, unchecked). The button has a text label. :doc:`Table ` Table of data :doc:`TextInput ` Text Input field :doc:`Tree ` Tree of data :doc:`WebView ` A panel for displaying HTML - :doc:`Widget ` The base widget -======================================================================= ==================================== + :doc:`Widget ` The abstract base class of all widgets. This class should not be be + instantiated directly. +======================================================================= ======================================================================== Layout widgets -------------- -==================================================================== ========================== +==================================================================== ======================================================================== Usage Description -==================================================================== ========================== - :doc:`Box ` Container for components +==================================================================== ======================================================================== + :doc:`Box ` A generic container for other widgets. Used to construct layouts. :doc:`ScrollContainer ` Scrollable Container :doc:`SplitContainer ` Split Container :doc:`OptionContainer ` Option Container -==================================================================== ========================== +==================================================================== ======================================================================== Resources --------- diff --git a/docs/reference/api/widgets/activityindicator.rst b/docs/reference/api/widgets/activityindicator.rst index 7c3f796b7d..3570649528 100644 --- a/docs/reference/api/widgets/activityindicator.rst +++ b/docs/reference/api/widgets/activityindicator.rst @@ -1,5 +1,5 @@ -Activity Indicator -================== +ActivityIndicator +================= A small animated indicator showing activity on a task of indeterminate length, usually rendered as a "spinner" animation. diff --git a/docs/reference/api/widgets/button.rst b/docs/reference/api/widgets/button.rst index 2470364ddc..a847c6133d 100644 --- a/docs/reference/api/widgets/button.rst +++ b/docs/reference/api/widgets/button.rst @@ -1,7 +1,7 @@ Button ====== -A widget that can be pressed or clicked to cause an action in an application. +A button that can be pressed or clicked. .. figure:: /reference/images/Button.jpeg :align: center diff --git a/docs/reference/api/widgets/imageview.rst b/docs/reference/api/widgets/imageview.rst index 800bbfdf34..2a1e6775ea 100644 --- a/docs/reference/api/widgets/imageview.rst +++ b/docs/reference/api/widgets/imageview.rst @@ -1,5 +1,5 @@ -Image View -========== +ImageView +========= .. rst-class:: widget-support .. csv-filter:: Availability (:ref:`Key `) diff --git a/docs/reference/api/widgets/multilinetextinput.rst b/docs/reference/api/widgets/multilinetextinput.rst index ca2b5321e6..4ca41c84f5 100644 --- a/docs/reference/api/widgets/multilinetextinput.rst +++ b/docs/reference/api/widgets/multilinetextinput.rst @@ -1,5 +1,11 @@ -Multi-line text input -===================== +MultilineTextInput +================== + +A scrollable panel that allows for the display and editing of multiple lines of text. + +.. figure:: /reference/images/MultilineTextInput.png + :align: center + :width: 300 .. rst-class:: widget-support .. csv-filter:: Availability (:ref:`Key `) @@ -8,8 +14,6 @@ Multi-line text input :included_cols: 4,5,6,7,8,9 :exclude: {0: '(?!^(MultilineTextInput|Component)$)'} -The Multi-line text input is similar to the text input but designed for larger inputs, similar to the ``