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

Add scroll_to_top/bottom for MultilineTextInput on iOS #1876

Closed
MarkusPiotrowski opened this issue Apr 16, 2023 · 13 comments
Closed

Add scroll_to_top/bottom for MultilineTextInput on iOS #1876

MarkusPiotrowski opened this issue Apr 16, 2023 · 13 comments
Labels
enhancement New features, or improvements to existing features. good first issue Is this your first time contributing? This could be a good place to start! iOS The issue relates to Apple iOS mobile support.

Comments

@MarkusPiotrowski
Copy link
Contributor

What is the problem or limitation you are having?

With PR #1728 I added scroll_to_top and scroll_to_bottom functions for the MultilineTextInput widget for Windows, macOS, Linux and Android.

Possibly, I found an implementation for iOS also, unfortunately, due to hardware limitations, I'm not able to test this by myself. I'd be happy if someone would pick this up, test it and file a PR.

In toga/iOS/src/toga_iOS/widgets/multilinetextinput.py replace the scroll_to_bottom() / scroll_to_top() functions with the following code and import NSRange from toga_iOS.libs:

...
from toga_iOS.libs import (
    ...
    NSRange,
    ...

...
    def scroll_to_bottom(self):
        range_ = NSRange.NSMakeRange(len(self.native.text) - 1, 1)
        self.native.scrollRangeToVisible(range_)

    def scroll_to_top(self):
        range_ = NSRange.NSMakeRange(0, 0)
        self.native.scrollRangeToVisible(range_)

You can use the MultilineTextInput example for testing.

Describe the solution you'd like

Implementation of scroll_to_bottom()/scroll_to_top() for MultilineTextInput on the iOS platform.

Describe alternatives you've considered

I'd also be happy to do the PR myself, as soon as I know that the code is working as intended.

Additional context

No response

@MarkusPiotrowski MarkusPiotrowski added the enhancement New features, or improvements to existing features. label Apr 16, 2023
@freakboy3742 freakboy3742 added good first issue Is this your first time contributing? This could be a good place to start! iOS The issue relates to Apple iOS mobile support. labels Apr 17, 2023
@kunalvirk
Copy link
Contributor

Hi @MarkusPiotrowski,

I can pick this up. As it will be my first contribution to an open source project can you please help me? I have setup a dev environment and replaced the code snippet.

Thanks,

@MarkusPiotrowski
Copy link
Contributor Author

@kunalvirk I'll try.

  1. This issue is related to iPhone apps, so I'd recommend that you first set up a briefcase environment and see if you can make a simple iOS app that runs on an iPhone or an iPhone simulator. I don't know if you have already done so, if not I would follow the tutorial here
  2. I would then change toga/iOS/src/toga_iOS/widgets/multilinetextinput.py in the local installation of your briefcase environment. I don't know where this is located on a Mac, possibly something like /private/tmp/.venv/lib/python3.8/site-packages? (Alternatively, you can change the pyproject.toml file to link to your toga dev installation)
  3. Make a new project and use the MultilineTextInput example as your app. See if it runs as expected with the scroll_to_bottm/scroll_to_top buttons.

When we know that it is working, we can go ahead with the pull request. Have you some experience with git? I strongly recommend to never make changes in the main branch. Always work on a new branch.

@kunalvirk
Copy link
Contributor

Thanks for the revert. I will follow the steps that you have mentioned and update here soon.
Yes, I do have experience with git. I will create/make any change in my own branch.

Thanks again,

@kunalvirk
Copy link
Contributor

Hi,

So I tried setting up a basic iOS app with MultilineTextInput example and ran it with xcode. The "Go to top" and "Go to bottom" buttons are not working.

P.S. : For toga, I cloned and edited the file as you have mentioned and installed that using pip install -e

image

@MarkusPiotrowski
Copy link
Contributor Author

MarkusPiotrowski commented May 4, 2023

It's hard to judge from this screenshot what is actually going wrong.
Can you generate some output from a console or inspect the log files? It would be nice to know if it throws some errors in the background when you press the buttons. Furthermore, it would also be nice to know if the app is really using the modified multilinetextinput so maybe you can add some code to the scroll_to... methods e.g. to display some text in the widget.

@kunalvirk
Copy link
Contributor

Oops my bad. I made a mistake while building this. Now it is throwing an error in importing NSRange from toga_iOS.libs.

This is how the multilinetextinput.py file looks like

from rubicon.objc import CGPoint, objc_method, objc_property
from travertino.size import at_least

from toga_iOS.libs import (
    NSLayoutAttributeBottom,
    NSLayoutAttributeLeading,
    NSLayoutAttributeTop,
    NSLayoutAttributeTrailing,
    NSLayoutConstraint,
    NSLayoutRelationEqual,
    UILabel,
    UITextView,
    NSRange
)
from toga_iOS.widgets.base import Widget


class TogaMultilineTextView(UITextView):
    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def pointInside_withEvent_(self, point: CGPoint, event) -> bool:
        # To keep consistency with non-mobile platforms, we'll resign the
        # responder status when you tap somewhere else outside this view
        # (except the keyboard)
        within_x = 0 < point.x < self.frame.size.width
        within_y = 0 < point.y < self.frame.size.height
        in_view = within_x and within_y
        if not in_view:
            self.resignFirstResponder()
        return in_view

    @objc_method
    def textViewShouldEndEditing_(self, text_view):
        return True

    @objc_method
    def textViewDidBeginEditing_(self, text_view):
        self.placeholder_label.setHidden_(True)

    @objc_method
    def textViewDidEndEditing_(self, text_view):
        self.placeholder_label.setHidden_(len(text_view.text) > 0)


class MultilineTextInput(Widget):
    def create(self):
        self.native = TogaMultilineTextView.alloc().init()
        self.native.interface = self.interface
        self.native.impl = self
        self.native.delegate = self.native

        # Placeholder isn't natively supported, so we create our
        # own
        self.placeholder_label = UILabel.alloc().init()
        self.placeholder_label.translatesAutoresizingMaskIntoConstraints = False
        self.placeholder_label.font = self.native.font
        self.placeholder_label.alpha = 0.5
        self.native.addSubview_(self.placeholder_label)
        self.constrain_placeholder_label()

        # Delegate needs to update the placeholder depending on
        # input, so we give it just that to avoid a retain cycle
        self.native.placeholder_label = self.placeholder_label

        self.add_constraints()

    def constrain_placeholder_label(self):
        leading_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.placeholder_label,
            NSLayoutAttributeLeading,
            NSLayoutRelationEqual,
            self.native,
            NSLayoutAttributeLeading,
            1.0,
            4.0,
        )
        trailing_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.placeholder_label,
            NSLayoutAttributeTrailing,
            NSLayoutRelationEqual,
            self.native,
            NSLayoutAttributeTrailing,
            1.0,
            0,
        )
        top_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.placeholder_label,
            NSLayoutAttributeTop,
            NSLayoutRelationEqual,
            self.native,
            NSLayoutAttributeTop,
            1.0,
            8.0,
        )
        bottom_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.placeholder_label,
            NSLayoutAttributeBottom,
            NSLayoutRelationEqual,
            self.native,
            NSLayoutAttributeBottom,
            1.0,
            0,
        )
        self.native.addConstraints_(
            [leading_constraint, trailing_constraint, top_constraint, bottom_constraint]
        )

    def set_placeholder(self, value):
        self.placeholder_label.text = value

    def set_readonly(self, value):
        self.native.editable = not value

    def set_value(self, value):
        self.native.text = value
        self.placeholder_label.setHidden_(len(self.native.text) > 0)

    def get_value(self):
        return self.native.text

    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_font(self, font):
        if font:
            native_font = font._impl.native
            self.native.font = native_font
            self.placeholder_label.font = native_font

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

    def scroll_to_bottom(self):
        print("[DEV] :: Scrolling to bottom")
        range_ = NSRange.NSMakeRange(len(self.native.text) - 1, 1)
        self.native.scrollRangeToVisible(range_)

    def scroll_to_top(self):
        print("[DEV] :: Scrolling to top")
        range_ = NSRange.NSMakeRange(0, 0)
        self.native.scrollRangeToVisible(range_)

And this is what I get in console after the build

ImportError: cannot import name 'NSRange' from 'toga_iOS.libs' (/Users/kunalvirk/Library/Developer/CoreSimulator/Devices/3588F52C-69A1-4437-9BE1-0C3A607D8EB4/data/Containers/Bundle/Application/EE68202E-2C51-4304-A7F3-105AE6F89861/Hello World.app/app_packages/toga_iOS/libs/__init__.py)

@freakboy3742
Copy link
Member

freakboy3742 commented May 4, 2023

ImportError: cannot import name 'NSRange' from 'toga_iOS.libs' (/Users/kunalvirk/Library/Developer/CoreSimulator/Devices/3588F52C-69A1-4437-9BE1-0C3A607D8EB4/data/Containers/Bundle/Application/EE68202E-2C51-4304-A7F3-105AE6F89861/Hello World.app/app_packages/toga_iOS/libs/__init__.py)

Well... yes - because NSRange isn't part of toga_iOS.libs. It's defined in rubicon.objc. There's nothing special going on here. Classes don't magically appear in a namespace because it's running on iOS; they need to be defined, or they need to be imported from somewhere they are defined.

@MarkusPiotrowski
Copy link
Contributor Author

It's me to blame; if I remember correctly, I derived these methods from this SO question. And seeing that there are a number of NS... imports from toga_iOS.libs in this widget code here, I assumed, without checking it, that NSRange could also be imported from there (as said, I raised this issue because I don't have the hardware for testing this). Thanks for pointing us in the right direction.

@kunalvirk So, can you try from rubicon.objc import ..., NSRange, ... ?

@kunalvirk
Copy link
Contributor

Yes I tried it with from rubicon.objc import ..., NSRange, ... and I did some google and found this SO question

Then I updated the scroll methods like below after importing it from rubicon.objc

def scroll_to_bottom(self):
        print("[DEV] :: Scrolling to bottom")
        range_ = NSRange(len(self.native.text) - 1, 1)
        self.native.scrollRangeToVisible(range_)

    def scroll_to_top(self):
        print("[DEV] :: Scrolling to top")
        range_ = NSRange(0, 0)
        self.native.scrollRangeToVisible(range_)

This actually worked.

@MarkusPiotrowski
Copy link
Contributor Author

Great!

Next thing to do is to write a file 1876.feature.rst file in the folder changes. The content could be something like:
Programmatically scrolling to top and bottom in MultilineTextInput is now possible on iOS

I don't think that we need to supply a test. It is already implemented for the other platforms. We'll see when doing the PR.

Have a look here: https://toga.readthedocs.io/en/stable/how-to/contribute-code.html , if necessary, and then you can file the PR!

@kunalvirk
Copy link
Contributor

I have opened a PR, please review #1929.

@MarkusPiotrowski
Copy link
Contributor Author

Solved with merging #1929.

@kunalvirk
Copy link
Contributor

Thanks @MarkusPiotrowski for the support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New features, or improvements to existing features. good first issue Is this your first time contributing? This could be a good place to start! iOS The issue relates to Apple iOS mobile support.
Projects
None yet
Development

No branches or pull requests

3 participants