rawpython / remi

Python REMote Interface library. Platform independent. In about 100 Kbytes, perfect for your diet.
Apache License 2.0
3.49k stars 401 forks source link

PySimpleGUI Port #273

Open MikeTheWatchGuy opened 5 years ago

MikeTheWatchGuy commented 5 years ago

It's been suggested that PySimpleGUI be ported to run on top of Remi.

PySimpleGUI has been ported to tkinter, Qt, and WxPython. Sure would be fun to run it on Remi too.

Is this something that you think is possible?

The architecture of PySimpleGUI's "event loop" needs to be able to call the underlying GUI framework's "mainloop" in both a non-blocking way and have the ability to "cancel" a mainloop after a set amount of time is elapsed. Or, if it's possible to put a "timeout" on the mainloop then a timer is not needed.

The Architecture is discussed here.

Can Remi provide these services?

I'm sorry that I have not yet done much development in Remi.... still learning and want to learn more. Hope to be able to learn the answers to these questions myself, but would be helpful to get a jump start if possible.

MikeTheWatchGuy commented 5 years ago

Having spent the day playing with Remi I think that I can do a PySimpleGUI port! I think the best approach will be to run Remi in a thread so that I can run the PySimpleGUI "event loop" in a non-blocking fashion. I'll let the thread block instead of the application.

This is actually pretty exciting! I hope to have a port up and running this week.

dddomodossola commented 5 years ago

Hello @MikeTheWatchGuy ,

It would be cool to have Remi an alternative in PySImpleGUI. The remi event loop is not interruptable (as you maybe noticed) and can't terminate after a timeout. However AFAIK it should be the same for Qt.

I'm not sure to understand why you should stop the event loop. Be aware that the App.idle loop is not related to the event loop. Events are triggered by websocket actions.

How would you manage the access from multiple users?

Please be patient for the long delay between replies, I'm really busy, Have a good development Best Regards

MikeTheWatchGuy commented 5 years ago

Oh wow, I didn't expect this fast of a response! Thanks for your time.

I'm super-excited about this port! This is going to be so cool! You've really opened up a lot to Python programmers. I'll try to keep the requests for your time to a minimum. Hoping to have something up and running this week.

dddomodossola commented 5 years ago

@MikeTheWatchGuy ok, keep us informed. Here I am for questions or suggestions. Thank you

dddomodossola commented 5 years ago

@MikeTheWatchGuy Do you know that remi has a graphical gui editor? Here is a live example http://remiguieditor--daviderosa.repl.co/ (however I suggest to use it locally). Maybe you can use it to explore some remi widgets.

MikeTheWatchGuy commented 5 years ago

Thanks so much for offering to help.

I've got a question...

How can I pass in a parameter into the MyApp class? I need to provide access to a variable from within the methods of the MyApp class.

At the moment the MyApp class is self-contained, isolated using only the resources that are defined within the class. I hope this makes sense.


Oops... nevermind... just found a post where you discuss user data in the start call. Sorry to bug you...

dddomodossola commented 5 years ago

Here is an example:

class MyApp(App):
    def main(self, param1, param2, param3):
        main_container = gui.VBox(width=300, height=200, style={'margin':'0px auto'})
        main_container.append(gui.Label("PARAMETERS %s %s %s"%(param1, param2, param3)))
        return main_container

if __name__ == "__main__":
    # starts the webserver
    start(MyApp, address='0.0.0.0', port=0, start_browser=True, userdata=('param1', 'param2', 3))
MikeTheWatchGuy commented 5 years ago

Poof! I'm up and running! PySimpleGUI code "running in a browser window". It's pretty extraordinary to see and experience. Never thought something like this would be possible.

I've released a super-early version to PyPI and GitHub as PySimpleGUIWeb.

You've made it really easy to do this. The Remi interface is easy to work with.

So far I have the Text, Input Text and Button "Elements" (PySimpleGUI widgets) up and running.

This code

import PySimpleGUIWeb as sg

def main():
    layout = [
                [sg.Text('This is a text element')],
                [sg.Text('Here is another ' ), sg.Text(' and another on the same line')],
                [sg.Text('If you close the browser tab, the app will exit gracefully')],
                [sg.InputText('Source'), sg.FolderBrowse()],
                [sg.InputText('Dest'), sg.FolderBrowse()],
                [sg.Ok(), sg.Cancel()]
            ]

    window = sg.Window('Demo window..').Layout(layout)
    while True:
        event, values = window.Read()
        print(event, values)
        if event is None:
            break
    window.Close()

main()
print('Program terminating normally')

Produced this window

image

Brianzhengca commented 5 years ago

@MikeTheWatchGuy Do you know where can I download pysimpleguiweb??? Thank you

MikeTheWatchGuy commented 5 years ago

You can download the .py file from the GitHub or you can pip install it pip install pysimpleguiweb

Be warned that it's super super early. I've only got enough going to show that it's going to work. I can display text, get text from the input fields, and read buttons. It's got a long ways to go (but progress should be quick). You could start with tkinter (or Qt or WxPython) and move over to this Remi port later. The PySimpleGUI source code is meant to be 100% compatible across the platforms.

Remi is an awesome framework to work with. It's making this super easy to do.

Brianzhengca commented 5 years ago

@MikeTheWatchGuy Thank you I am going to try it out soon ;)

Brianzhengca commented 5 years ago

I think it will be great if the user can use their own css files. So instead of super(Window.MyApp, self).init(args) on line 3113, I think we can do: try: super(MyApp, self).init(args, static_file_path={'res':'res'}) except: super(MyApp, self).init(*args)

MikeTheWatchGuy commented 5 years ago

A couple questions

  1. What's the best way to turn off logging? It's working great and thus don't need the debug output. I set the level at INFO (and tried other levels) and I stilled get 3 messages.

  2. Where is the best location for documentation? Things like what are all the parms to the start call would be great.


Update.... (hope it's OK to update you on a few thing... you've really enabled some cool stuff.. without Remi none of this would be possible)

Having a ball working with the API. Today adding fonts, colors, window title, etc. It's so cool to see this stuff running in Chrome yet still executing my Python code.

I have a ton of demo programs that help me bring up ports. They require more and more features. I got this timer up an running. It does a lot of stuff like creating the window, updating the text very frequently, changing button text when the pause button clicked, closes the window when exit clicked. It's not bad for day 1 of the port.

web timer

MikeTheWatchGuy commented 5 years ago

@Brianzhengca let's keep the PySimpleGUI Issues discussed on the PySimpleGUI GitHub.

dddomodossola commented 5 years ago

@MikeTheWatchGuy COOL! Unfortunately remi hasn't documentation yet.. Look at the code and the Readme file, or simply ask me. To solve the logging problem:

import remi.server as server
import logging
class MyApp(App):
    #this is required to override the BaseHTTPRequestHandler logger
    def log_message(self, *args, **kwargs):
        pass

if __name__ == "__main__":
    logging.getLogger('remi').disabled = True
    logging.getLogger('remi.server.ws').disabled = True
    logging.getLogger('remi.server').disabled = True
    logging.getLogger('remi.request').disabled = True
    #use this code to start the application instead of the **start** call
    s = server.Server(MyApp, start=True, address='0.0.0.0', port=8081, start_browser=True, multiple_instance=True)

Have a good code development

MikeTheWatchGuy commented 5 years ago

I've noticed that I no longer get exceptions when running my code. This is code that's running inside of the MyApp object.

Do I need to do something so that I see exceptions again?

I have write "perfect code" at the moment since I don't see any crashes.

Thank you for your help.

I will capture all of these things you are posting and put into a FAQ, readme, ..., something that you can use.

dddomodossola commented 5 years ago

Hello @MikeTheWatchGuy ,

Instead logging.getLogger(...).disabled=True you can set the desired log level as you already know i.e. logging.getLogger('remi').setLevel(logging.WARNING). It should work as you expect.

Don't worry about making the FAQ for remi with these info, these will stay available in this issue/thread. ;-)

MikeTheWatchGuy commented 5 years ago

Docs

You've actually done a fantastic job of answering everyone's questions. There is a TON of stuff on gitter and a lot in these GitHub Issues. It just needs to be consolidated. The package is solid and it documents well.

For example, I just discovered how to set tooltips, another milestone, thanks to you answering a question. I'm simply keeping a list of these as I find them. I'll make a little document for myself and you're welcome to use it or not. While I don't enjoy docs, I see enormous value in them which is why PySimpleGUI is pretty heavily documented. I get comments, often, saying my package was chosen because of the documentation.

You did a great job on the example programs. I'm searching them several times a day.

Update

Managed to nail 3 more widgets last night (combobox, checkbox, listbox). Working my way through them all!

Today I added a multiline input and multiline output capabilities.


Question

Is it possible to "Append" text to a multiline text widget? This for my scrolling text output.

At the moment the only thing I can think of doing is to keep track of the entire contents of the widget that I append new text onto and re-write the entire block of data into the widget. It's quite inefficient.


Logging

When I use only the new line (setLevel warning), I continue to get a bunch of log messages.

Even with a bunch of code

        logging.getLogger('remi').setLevel(logging.WARNING)
        logging.getLogger('remi').disabled = True
        logging.getLogger('remi.server.ws').disabled = True
        logging.getLogger('remi.server').disabled = True
        logging.getLogger('remi.request').disabled = True

I still get 2 log messages.

127.0.0.1 - - [24/Jan/2019 12:46:28] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Jan/2019 12:46:28] "GET /res:style.css HTTP/1.1" 200 -
MikeTheWatchGuy commented 5 years ago

Reading Input Text Value in Key Event

I have a feature that returns events as elements are changed. If a character is typed into an input field, or a slider moved, etc. Your SDK is awesome to enabling this feature.

I am trying to get it working for an Input Text element. I get the callback just fine and the correct character is being passed in via the keycode parameter.

But, when I read the current "value" of the Input Text element it returns the initial value / previous value, not the new value.

For example, if my Input Text element is initialized with the value "Test" and I've enabled key stroke events, and then I highlight "Test" and replace it with "123". I get the keystroke callbacks for each of the characters 1, 2, 3. But, when I call widget.get_value() it returns "Test". It's only after I click a button that the get_value returns the new values.

dddomodossola commented 5 years ago

@MikeTheWatchGuy Thank you a lot!

  1. Logging problem: You maybe forgot to oveload the aforementioned method:

    class MyApp(App):
        #this is required to override the BaseHTTPRequestHandler logger
         def log_message(self, *args, **kwargs):
            pass
  2. Text append: you should do something like:

    mytextwidget.set_value( mytextwidget.get_value() + text_to_append )
  3. get_value() returns the old text: onkeydown and onkeyup events doesn't directly update the widget value improving text consistency and preventing user interaction glitches. You however receives the updated value in the event parameters. onkeyup and onkeydown events provide the key and new_value.

MikeTheWatchGuy commented 5 years ago

Exceptions.....

I don't seem to be getting any exceptions when I have an error in my code. PyCharm isn't breaking like it normally would. Is there something special I need to do to enable exceptions again? It's difficult to debug without seeing where the problems are.

Input Text Callbacks Always Upper Case

The keycode passed into the Input Text callback shows upper case characters. Maybe I'm converting the keycode incorrectly? Is this a good way of getting the character?
character = char(int(keycode))

It seems like it's literally they KEY from the keyboard and that you have to know the status of the shift key to know what the actual typed character is. Surely I'm doing this wrong.

Spinner With Text Choices

My "Spinner Element" that I provide through PySimpleGUI is text based. Instead of a range of numbers it's a list of strings. I can still do numeric entries this way as well as selecting strings.

In Qt I had to implement this as a combined spinbox and a text entry field. Is it possible to do something similar in Remi? Can I get just the up/down arrows and add my own text entry widget?

Documentation

You've actually done quite a bit of docs. I found a PDF from December which was generated from the code best I can tell. I'm interested in doing something similar some day in PySimpleGUI so I want to study your code.

It will be helpful to add a FAQ / Common Operations document that consolidates the information you've so patiently posted all over the internet, especially on Gitter. I'm happy to collect up my notes and post them to help others following after me.

Radio Buttons

I see that Radio Buttons are absent. I think I could implement them using checkboxes, but it would be nice to change the graphic from a checkbox to a bullet.

How would you go about implementing a Radio Button capability? It doesn't have to be perfectly done. I'm interested in functionality over everything else.

GitHub Issues / Questions

I expect to keep asking questions as I continue my port. Should I continue to bulk them up into this single Issue or break them out into single issues that can be closed when we're doing with them? I expect they won't remain open for long. It gives up the ability to track requests individually as well as keep the conversation focused to a single topic. It will be helpful for future readers perhaps.

While I think it would be a good idea to post them individually, I don't want to create a feeling of being swamped with requests by posting 4 or 5 Issues at a time.

The way PySimpleGUI works it that pretty much every feature of Remi, tkinter, Qt, WxPython is wrapped in the PySimpleGUI SDK. I make all of the frameworks look and act the same way. It means I hit every widget with almost every option in a framework.

Your Code is Amazing... The SDK is Super-Easy to follow...

The size of your code base is amazingly small yet the feature list is huge. You use the Python language in a way I've not seen before. I'm not used to working with decorators so it's going to be a while before I really grasp what you're doing.

What I really like is the interfaces you've provided. I SO wish all of the other GUI packages made it as easy as you do to handle events. WxPython is probably the most similar to your interface. Qt would be next after that.

MikeTheWatchGuy commented 5 years ago

Exception problem "solved" by adding my own try/except around my code that's in the main method. In the except portion I simply print the traceback: print(traceback.format_exc())

This is exactly the kind of thing I'm putting into a FAQ since I'm guessing other people have asked the same question at some point.

dddomodossola commented 5 years ago

@MikeTheWatchGuy here are the replies

Exceptions

you was unable to see exceptions because some logging sources are disabled in your code. I previously suggested you to replace logging.getLogger("...").disabled = True with logging.getLogger("...").setLevel(logging.WARNING). Here is an example of an App containing an exception, with a correct logging report:

import remi.gui as gui
from remi import start, App
import os
import logging

class MyApp(App):
    def main(self):
        #creating a container VBox type, vertical (you can use also HBox or Widget)
        main_container = gui.VBox(width=300, height=200, style={'margin':'0px auto'})
        main_container.onclick.connect()
        # returning the root widget
        return main_container

    def log_message(self, *args, **kwargs):
        pass

if __name__ == "__main__":
    logging.getLogger('remi').setLevel(logging.WARNING)
    logging.getLogger('remi.server.ws').setLevel(logging.WARNING)
    logging.getLogger('remi.server').setLevel(logging.WARNING)
    logging.getLogger('remi.request').setLevel(logging.WARNING)
    # starts the webserver
    start(MyApp, address='0.0.0.0', port=0, start_browser=True, username=None, password=None)

Input Text Callbacks Always Upper Case

Keycode is always Uppercase for onkeyup and onkeydown events. I was not aware of this, but it's a standard for the browsers. However, since the base Widget already has these events with additional information (like the pressed key, not only the keycode), the solution is simple. We can override these events, copying the implementation from the Widget class:

import remi.gui as gui
from remi import start, App
import os

class TextInput_raw_onkeyup(gui.TextInput):
    @gui.decorate_set_on_listener("(self, emitter, key, keycode, ctrl, shift, alt)")
    @gui.decorate_event_js("""var params={};params['key']=event.key;
            params['keycode']=(event.which||event.keyCode);
            params['ctrl']=event.ctrlKey;
            params['shift']=event.shiftKey;
            params['alt']=event.altKey;
            sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);
            event.stopPropagation();event.preventDefault();return false;""")
    def onkeyup(self, key, keycode, ctrl, shift, alt):
        return (key, keycode, ctrl, shift, alt)

    @gui.decorate_set_on_listener("(self, emitter, key, keycode, ctrl, shift, alt)")
    @gui.decorate_event_js("""var params={};params['key']=event.key;
            params['keycode']=(event.which||event.keyCode);
            params['ctrl']=event.ctrlKey;
            params['shift']=event.shiftKey;
            params['alt']=event.altKey;
            sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);
            event.stopPropagation();event.preventDefault();return false;""")
    def onkeydown(self, key, keycode, ctrl, shift, alt):
        return (key, keycode, ctrl, shift, alt)

class MyApp(App):
    def main(self):
        main_container = gui.VBox(width=300, style={'margin':'0px auto'})
        txt = TextInput_raw_onkeyup(True,'type here')
        main_container.append(txt)
        txt.onkeyup.connect(self.onkey)
        self.lbl = gui.Label("", style={'font-size':'18px'})
        main_container.append(self.lbl)
        return main_container

    def onkey(self, emitter, key, keycode, ctrl, shift, alt):
        self.lbl.set_text("keyup keycode:" + keycode + " char:" + key)

if __name__ == "__main__":
    start(MyApp, address='0.0.0.0', port=0, start_browser=True, username=None, password=None)

Spinner With Text Choices

Here is a simple implementation:

class Spinner(gui.GridBox):
    def __init__(self, choices, *args, **kwargs):
        super(Spinner, self).__init__(*args, **kwargs)
        self.index = 0
        self.choices = choices
        self.define_grid(['ab', 'ac'])
        self.value = gui.Label("", width="100%", height="100%", style={'text-align':'center'}) #of course you can use a TextInput instead
        self.bt_up = gui.Button("+", width="100%", height="100%") #you can also setup an Image with arrows
        self.bt_down = gui.Button("-", width="100%", height="100%")
        self.bt_up.onclick.connect(self.up)
        self.bt_down.onclick.connect(self.down)
        self.append({'a':self.value, 'b':self.bt_up, 'c':self.bt_down})
        self.style.update({'grid-template-columns':'80% 20%', 'grid-template-rows':'50% 50%'})
        self.up(None) #setting up the first value
        self.attributes['tabindex'] ='0' #enables focusing
        self.onkeyup.connect(self.onkey) #change value by keyboard arrows

    def up(self, emitter):
        self.index = max(0,self.index-1) #this prevents negative values
        self.value.set_text(self.choices[self.index])
        self.onchange()

    def down(self, emitter):
        self.index = min(len(self.choices)-1,self.index+1) #this prevents values outside choices len
        self.value.set_text(self.choices[self.index])
        self.onchange()

    def onkey(self, emitter, key, keycode, ctrl, shift, alt):
        if int(keycode)==38: #UP
            self.up(emitter)
        if int(keycode)==40: #DOWN
            self.down(emitter)

    @gui.decorate_event
    def onchange(self):
        return (self.choices[self.index],)

class MyApp(App):
    def main(self):
        main_container = gui.VBox(width=300, height=100, style={'margin':'0px auto'})
        self.spinner = Spinner(['one', 'two', 'three'], width=200, height=30, style={'border':'1px solid gray'})
        self.spinner.onchange.connect(self.spinner_changed)
        main_container.append(self.spinner)
        return main_container    

    def spinner_changed(self, emitter, value):
        print("spinner changed %s"%value)

if __name__ == "__main__":
    start(MyApp, address='0.0.0.0', port=0, start_browser=True, username=None, password=None)

Documentation

I hope in the future I will be able to create some kind of documentation/tutorials

Radio Buttons

The best way to represent a radiobutton would be using Svg elements. You can try to implement a radiobutton widget as for the Spinner widget. If you find it difficult I will help you.

GitHub Issues / Questions

Single issue allows to mute it if not interesting for someone. Multiple issues allows more controls/order. There is another option, the reddit remi channel reddit.com/r/remigui , I think this would be the best solution. I suppose this would be the best solution.

Code style and decorators

Personally I prefer plain code over decorators. I prefer the code when it is easy to understand. However some things need particular solutions. If something is unclear, ask me, I will explain ;-)

MikeTheWatchGuy commented 5 years ago

It looks like Remi and PySimpleGUIWeb are tracking each other when it comes to Pip installs..

Remi installs are on the left, PySimpleGUIWeb on the right. What's a bit baffling is the installs on Jan 24th. There were over 1,000 installs of PySimpleGUIWeb but the Remi installs didn't go up. This was prior to adding Remi as a requirement on PyPI. Remi had to be manually installed at that time. Now Remi is automatically installed.

snag-0317

dddomodossola commented 5 years ago

so much donwloads @MikeTheWatchGuy :D but however some of them are automatic pip installs from repl.it .

MikeTheWatchGuy commented 5 years ago

Damn, we really hit a high mark over the past few days! Even if some of this is repl.it traffic, it's really impressive!

image