remi icon indicating copy to clipboard operation
remi copied to clipboard

[Enhancement / Code Help] Scrolling to bottom of Text Input / Output Widget

Open MikeTheWatchGuy opened this issue 5 years ago • 21 comments

I use the TextInput also as an "output" widget. There are 2 ways I do this. One is via re-routing stdout. In this case, the TextInput widget is updated by Remi itself.

This is the code I was given that should scroll to the end of the widget:

            if hasattr(app, "websockets"):
                app.execute_javascript("document.getElementById('%s').scrollTop=%s;" % (
                    self.Widget.identifier, 9999))  # 9999 number of pixel to scroll

This works when running within the Remi thread, but it does not work when running as the user thread. What happens is that the jump happens, but then it jumps right back up to the top of the area.

I used the exact same code, just in 2 places. The place where Remi called it directly it works, where my user code calls it, it does not work.

Here is an example. The output box on TOP is updated using Remi's thread. The output box on the bottom is done via user code.

scroll issue

MikeTheWatchGuy avatar May 09 '19 22:05 MikeTheWatchGuy

I continue to struggle with the scrolling widgets.

I really need the ability for my Output Element to be able to convert the users "print" statements into a scrolling portion of the window. Everything works except for the scrolling behavior.

How can I help you get setup to lend a hand? Do you need an example PySimpleGUIWeb program that more specifically shows you the problem?

MikeTheWatchGuy avatar Aug 09 '19 19:08 MikeTheWatchGuy

Hello @MikeTheWatchGuy I will work directly on PySimpleGuiWeb to fix this. I planned to do this on Sunday afternoon. Will be fixed really soon.

dddomodossola avatar Aug 09 '19 20:08 dddomodossola

Thank you!

Let me know how I can help. I will code up some test code that is short and succinctly demonstrates the problems.

(thank you again for all the recent help!!!!!!!)

MikeTheWatchGuy avatar Aug 09 '19 23:08 MikeTheWatchGuy

Hello @MikeTheWatchGuy , as promised here is the fixed version of your MultilineOutput widget. Now the autoscroll works correctly ;-)

    # ---------------------------------------------------------------------- #
    #                           Multiline Output                             #
    # ---------------------------------------------------------------------- #
    class MultilineOutput(Element):
        def __init__(self, default_text='', enter_submits=False, disabled=False, autoscroll=False, size=(None, None), auto_size_text=None, background_color=None, text_color=None, change_submits=False, enable_events=False, do_not_clear=True, key=None, focus=False, font=None, pad=None, tooltip=None, visible=True, size_px=(None,None)):
            '''
            Multiline Element
            :param default_text:
            :param enter_submits:
            :param disabled:
            :param autoscroll:
            :param size:
            :param auto_size_text:
            :param background_color:
            :param text_color:
            :param do_not_clear:
            :param key:
            :param focus:
            :param pad:
            :param tooltip:
            :param font:
            '''
            self.DefaultText = default_text
            self.EnterSubmits = enter_submits
            bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
            self.Focus = focus
            self.do_not_clear = do_not_clear
            fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
            self.Autoscroll = autoscroll
            self.Disabled = disabled
            self.ChangeSubmits = change_submits or enable_events
            tsize = size                # convert tkinter size to pixels
            if size[0] is not None and size[0] < 100:
                tsize = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1]
            self.Widget = None      # type: remi.gui.TextInput
            self.CurrentValue = ''

            super().__init__(ELEM_TYPE_MULTILINE_OUTPUT, size=tsize, auto_size_text=auto_size_text, background_color=bg,
                            text_color=fg, key=key, pad=pad, tooltip=tooltip, font=font or DEFAULT_FONT, visible=visible, size_px=size_px)
            return


        def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None):
            if value is not None and not append:
                self.Widget.set_value(str(value))
            elif value is not None and append:
                self.CurrentValue = self.CurrentValue + '\n' + str(value)
                self.Widget.set_value(self.CurrentValue)
                self.Widget._set_updated()
                app = self.ParentForm.App
                
                if hasattr(app, "websockets"):
                    app.execute_javascript('element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; if(%(autoscroll)s){element.scrollTop=999999;} ' % {
                        "id":self.Widget.identifier, "content":self.Widget.get_value(), "autoscroll":'true' if self.Autoscroll else 'false'})
                
            super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible)


        def Get(self):
            self.WxTextCtrl.GetValue()

        def SetFocus(self):
            self.WxTextCtrl.SetFocus()

        def __del__(self):
            super().__del__()

dddomodossola avatar Aug 11 '19 10:08 dddomodossola

@MikeTheWatchGuy have you tried this fix?

dddomodossola avatar Aug 12 '19 04:08 dddomodossola

I have not yet but will today. I'm sorry I was slammed through the weekend! Still trying to push out 2 releases along with synchronized docs.

MikeTheWatchGuy avatar Aug 12 '19 12:08 MikeTheWatchGuy

OK! This indeed did work for the MultilineOutput element!!!!

Now I'm trying to fix the same problem on my Output Element. This element re-reroutes stderr and stdout.

When I'm creating the window, this his how I translate it into Remi Widgets:

                # -------------------------  OUTPUT element  ------------------------- #
            elif element_type == ELEM_TYPE_OUTPUT:
                element=element  # type: Output
                element.Widget = remi.gui.TextInput(single_line=False)
                element.Disabled = True
                do_font_and_color(element.Widget)
                tk_row_frame.append(element.Widget)
                toplevel_form.OutputElementForStdOut = element
                Window.stdout_is_rerouted = True
                Window.stdout_string_io = StringIO()
                sys.stdout = Window.stdout_string_io

There is no "Update" method that's called. It's all done internal to Remi, I think!

Can you help me with this one too. It should be the last of these scrolling elements for you to deal with. Sorry to keep asking for help!

MikeTheWatchGuy avatar Aug 15 '19 00:08 MikeTheWatchGuy

You know, I could SWEAR that that the "Output element" is working in the first screen shot I took, because it's being controlled directly by Remi, but yet now it's not scrolling correctly anymore.

Yea, that same code isn't working now. This is truly WEIRD.

MikeTheWatchGuy avatar Aug 15 '19 01:08 MikeTheWatchGuy

Hello @MikeTheWatchGuy I will do my best to fix this tomorrow evening. Don't worry to ask me for help. You supported remi a lot, I must and I'm pleased to collaborate with you on these "problems". Nonetheless I consider these issues as games to be solved :-) , I love code development.

dddomodossola avatar Aug 15 '19 10:08 dddomodossola

Hello @MikeTheWatchGuy, here is the working Output element. I tried it and it worked fine.

# ---------------------------------------------------------------------- #
#                           Output                                       #
#  Routes stdout, stderr to a scrolled window                            #
# ---------------------------------------------------------------------- #
class Output(Element):
    def __init__(self, size=(None, None), background_color=None, text_color=None, pad=None, font=None, tooltip=None,
                 key=None, visible=True, size_px=(None,None), disabled=False):
        '''
        Output Element
        :param size:
        :param background_color:
        :param text_color:
        :param pad:
        :param font:
        :param tooltip:
        :param key:
        '''
        self.Autoscroll = True
        bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
        # fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
        fg = text_color if text_color is not None else 'black' if DEFAULT_INPUT_TEXT_COLOR == COLOR_SYSTEM_DEFAULT else DEFAULT_INPUT_TEXT_COLOR
        self.Disabled = disabled
        self.Widget = None      # type: remi.gui.TextInput
        if size_px == (None, None) and size == (None, None):
            size = DEFAULT_OUTPUT_ELEMENT_SIZE
        if size[0] is not None and size[0] < 100:
            size = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1]
        super().__init__(ELEM_TYPE_OUTPUT, size=size, size_px=size_px, visible=visible, background_color=bg, text_color=fg, pad=pad, font=font, tooltip=tooltip, key=key)

    def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None):
        if value is not None and not append:
            self.Widget.set_value(str(value))
            self.CurrentValue = str(value)
        elif value is not None and append:
            self.CurrentValue = self.CurrentValue + '\n' + str(value)
            self.Widget.set_value(self.CurrentValue)
        self.Widget._set_updated()
        app = self.ParentForm.App
            
        if hasattr(app, "websockets"):
            app.execute_javascript('element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; if(%(autoscroll)s){element.scrollTop=999999;} ' % {
                "id":self.Widget.identifier, "content":self.Widget.get_value(), "autoscroll":'true' if self.Autoscroll else 'false'})
            
        super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible)

    def __del__(self):
        super().__del__()

dddomodossola avatar Aug 30 '19 22:08 dddomodossola

AWESOME!

That's two different things I need to integrate from you.

I'm afraid I've been so backed up with documentation and support of users that I've not been able to do any Remi work for weeks.

I'm hoping to get back to it as soon as I finish getting it PEP8 compliant. It's the one I've been working on lately.

I can't thank you enough for all the help!

PySimpleGUI avatar Aug 30 '19 23:08 PySimpleGUI

I'm struggling with autoscrolling when appending output once again.

Here's my update method. It's called when the user wants to output to a scrolling MultilineOutput element.

    def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None, autoscroll=None):
        self.Autoscroll = autoscroll is True            # convert to bool and save the autoscroll setting
        if value is not None and not append:
            self.Widget.set_value(str(value))
        elif value is not None and append:
            self.CurrentValue = self.CurrentValue + '\n' + str(value)
            self.Widget.set_value(self.CurrentValue)
            self.Widget._set_updated()
            app = self.ParentForm.App
            if hasattr(app, "websockets"):
                app.execute_javascript('element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; if(%(autoscroll)s){element.scrollTop=999999;} ' % {
                    "id":self.Widget.identifier, "content":self.Widget.get_value(), "autoscroll":'true' if self.Autoscroll else 'false'})

        super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible)

The problem is that the text is appending to the output just fine. The problem is that the view isn't scrolled to the bottom.

You can test this by running the PySimpleGUIWeb.py file. Running it brings up a test harness that looks like this:

image

If you press the OK button a new line is added to the scrolling Multiline output. If you keep clicking OK the box fills and begins to add to the text beyond the view. The widget isn't scrolling along with the output. (Help!)

PySimpleGUI avatar Feb 22 '20 14:02 PySimpleGUI

Hello @MikeTheWatchGuy which PSGW are you using? The master branch on github?

dddomodossola avatar Feb 22 '20 19:02 dddomodossola

Yes, the one checked into the master branch http://www.PySimpleGUI.com is what I'm running.

PySimpleGUI avatar Feb 22 '20 20:02 PySimpleGUI

Ok, I will test it right now. I'm sure it was fixed, maybe we lost the fix.

dddomodossola avatar Feb 22 '20 22:02 dddomodossola

Hello @PySimpleGUI, here is the corrected MultilineOutput version:

# ---------------------------------------------------------------------- #
#                           Multiline Output                             #
# ---------------------------------------------------------------------- #
class MultilineOutput(Element):
    def __init__(self, default_text='', enter_submits=False, disabled=False, autoscroll=False, size=(None, None), auto_size_text=None, background_color=None, text_color=None, change_submits=False, enable_events=False, do_not_clear=True, key=None, focus=False, font=None, pad=None, tooltip=None, visible=True, size_px=(None,None)):
        '''
        Multiline Element
        :param default_text:
        :param enter_submits:
        :param disabled:
        :param autoscroll:
        :param size:
        :param auto_size_text:
        :param background_color:
        :param text_color:
        :param do_not_clear:
        :param key:
        :param focus:
        :param pad:
        :param tooltip:
        :param font:
        '''
        self.DefaultText = default_text
        self.EnterSubmits = enter_submits
        bg = background_color if background_color else DEFAULT_INPUT_ELEMENTS_COLOR
        self.Focus = focus
        self.do_not_clear = do_not_clear
        fg = text_color if text_color is not None else DEFAULT_INPUT_TEXT_COLOR
        self.Autoscroll = autoscroll
        self.Disabled = disabled
        self.ChangeSubmits = change_submits or enable_events
        tsize = size                # convert tkinter size to pixels
        if size[0] is not None and size[0] < 100:
            tsize = size[0]*DEFAULT_PIXELS_TO_CHARS_SCALING[0], size[1]*DEFAULT_PIXELS_TO_CHARS_SCALING[1]
        self.Widget = None      # type: remi.gui.TextInput
        self.CurrentValue = ''

        super().__init__(ELEM_TYPE_MULTILINE_OUTPUT, size=tsize, auto_size_text=auto_size_text, background_color=bg,
                         text_color=fg, key=key, pad=pad, tooltip=tooltip, font=font or DEFAULT_FONT, visible=visible, size_px=size_px)
        return


    def Update(self, value=None, disabled=None, append=False, background_color=None, text_color=None, font=None, visible=None, autoscroll=None):
        self.Autoscroll = self.Autoscroll or autoscroll            # convert to bool and save the autoscroll setting
        if value is not None and not append:
            self.Widget.set_value(str(value))
            self.CurrentValue = str(value)
        elif value is not None and append:
            self.CurrentValue = self.CurrentValue + '\n' + str(value)
            self.Widget.set_value(self.CurrentValue)
        self.Widget._set_updated()
        app = self.ParentForm.App
            
        if hasattr(app, "websockets"):
            app.execute_javascript('element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; if(%(autoscroll)s){element.scrollTop=999999;} ' % {
                "id":self.Widget.identifier, "content":self.Widget.get_value(), "autoscroll":'true' if self.Autoscroll else 'false'})
            
        super().Update(self.Widget, background_color=background_color, text_color=text_color, font=font, visible=visible)

    update = Update

However there are some notes:

  1. The MultilineOuptut takes the autoscroll parameter in constructor AND also in the Update method. Why this parameter appears in two places?
  2. PSGW has MultilineOutput and Output classes. These are almost identical. Why this duplication?
  3. You have not included the Output element updated that I provided you in this same thread. You have still an old version.

Kind Regards

dddomodossola avatar Feb 22 '20 23:02 dddomodossola

Hmmm... I'm unsure about the line of code:

        self.Autoscroll = autoscroll is True            # convert to bool and save the autoscroll setting

I would have never written that line as I don't recall ever writing a "convert to bool" type of structure using "is True". The last place I got this code was from earlier in this listing.

I do see something similar in the tkinter code, just written differently. I would have thought that the one set when the element is created would be the DEFAULT going forward rather than getting changed when the update happens. It's a bug in my opinion that I can fix.

Output is indeed very similar. The difference is that Output reroutes standard out to the element. Multiline is what an Output element uses essentially.

I can modify the Output class with this fix.

Thanks for this!

PySimpleGUI avatar Feb 22 '20 23:02 PySimpleGUI

Oh crap... it was a bug in my code! What the heck happened??? Wow... I'm sorry to have sent this your direction. I'm confused now and will track down what happened. I don't get it.

PySimpleGUI avatar Feb 22 '20 23:02 PySimpleGUI

image

I appear to have made the bug when I poured some 900 additions to the file! DOH!

I'm sorry this one is on me. I don't know why I would have even added it, but there must have been a reason. Now I just need to make sure it does the right thing.

PySimpleGUI avatar Feb 22 '20 23:02 PySimpleGUI

This is the way the code should have read:

        autoscroll = self.Autoscroll if autoscroll is None else autoscroll

and then referenced down further

        if hasattr(app, "websockets"):
            app.execute_javascript(
                'element=document.getElementById("%(id)s"); element.innerHTML=`%(content)s`; if(%(autoscroll)s){element.scrollTop=999999;} ' % {
                    "id": self.Widget.identifier, "content": self.Widget.get_value(), "autoscroll": 'true' if autoscroll else 'false'})

This makes the initial setting the default and the user can override the default in a single update call.

PySimpleGUI avatar Feb 23 '20 00:02 PySimpleGUI

@MikeTheWatchGuy no problem, it can happen, the important thing is that it is fixed now ✌️

dddomodossola avatar Feb 23 '20 07:02 dddomodossola