From f27630721d2bbcfbce515dc2e6be3167f6a06a34 Mon Sep 17 00:00:00 2001 From: Yngve Levinsen Date: Fri, 11 Sep 2020 13:01:24 +0200 Subject: [PATCH 001/110] Pick up version from the git tag add remi.__version__ at runtime --- remi/__init__.py | 42 ++++++++++++++++++++++++++++++++++++++---- setup.py | 19 ++++++++++--------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/remi/__init__.py b/remi/__init__.py index c4d041d9..74378e4f 100644 --- a/remi/__init__.py +++ b/remi/__init__.py @@ -1,6 +1,40 @@ -from .gui import Widget, Button, TextInput, \ - SpinBox, Label, GenericDialog, InputDialog, ListView, ListItem, DropDown, DropDownItem, \ - Image, Table, TableRow, TableItem, TableTitle, Input, Slider, ColorPicker, Date, GenericObject, \ - FileFolderNavigator, FileFolderItem, FileSelectionDialog, Menu, MenuItem, FileUploader, FileDownloader, VideoPlayer +from .gui import ( + Widget, + Button, + TextInput, + SpinBox, + Label, + GenericDialog, + InputDialog, + ListView, + ListItem, + DropDown, + DropDownItem, + Image, + Table, + TableRow, + TableItem, + TableTitle, + Input, + Slider, + ColorPicker, + Date, + GenericObject, + FileFolderNavigator, + FileFolderItem, + FileSelectionDialog, + Menu, + MenuItem, + FileUploader, + FileDownloader, + VideoPlayer, +) from .server import App, Server, start +from pkg_resources import get_distribution, DistributionNotFound + +try: + __version__ = get_distribution(__name__).version +except DistributionNotFound: + # package is not installed + pass diff --git a/setup.py b/setup.py index 281ea259..7df2bb56 100644 --- a/setup.py +++ b/setup.py @@ -6,17 +6,18 @@ with open("README.md", "r") as fh: long_description = fh.read() -setup(name='remi', - version='2020.08.06', - description='Python REMote Interface library', +setup( + name="remi", + description="Python REMote Interface library", + use_scm_version=True, long_description=long_description, long_description_content_type="text/markdown", - url='https://github.com/dddomodossola/remi', - download_url='https://github.com/dddomodossola/remi/archive/master.zip', - keywords=['gui-library','remi','platform-independent','ui','gui'], - author='Davide Rosa', - author_email='dddomodossola@gmail.com', - license='Apache', + url="https://github.com/dddomodossola/remi", + download_url="https://github.com/dddomodossola/remi/archive/master.zip", + keywords=["gui-library", "remi", "platform-independent", "ui", "gui"], + author="Davide Rosa", + author_email="dddomodossola@gmail.com", + license="Apache", packages=setuptools.find_packages(), include_package_data=True, ) From ad222c430b7efac86e6fb4e417bd537a3838e6e0 Mon Sep 17 00:00:00 2001 From: Yngve Levinsen Date: Wed, 14 Oct 2020 13:00:58 +0200 Subject: [PATCH 002/110] Propose to throw an error on improper combo of settings for GridBox See #403 --- remi/gui.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/remi/gui.py b/remi/gui.py index 11b31e2e..d77bf92a 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -103,6 +103,13 @@ def to_uri(uri_data): return ("url('%s')" % uri_data) +class CssStyleError(Exception): + """ + Raised when (a combination of) settings will result in invalid/improper CSS + """ + pass + + class EventSource(object): def __init__(self, *args, **kwargs): self.setup_event_methods() @@ -1840,6 +1847,9 @@ def set_column_gap(self, value): Args: value (int or str): gap value (i.e. 10 or "10px") """ + if self.css_width == "auto": + if (type(value) == int and value != 0) or value[0] != "0": + raise CssStyleError("Do not set column gap in combination with width auto") if type(value) == int: value = str(value) + 'px' self.style['grid-column-gap'] = value @@ -1850,6 +1860,9 @@ def set_row_gap(self, value): Args: value (int or str): gap value (i.e. 10 or "10px") """ + if self.css_height == "auto": + if (type(value) == int and value != 0) or value[0] != "0": + raise CssStyleError("Do not set row gap in combination with height auto") if type(value) == int: value = str(value) + 'px' self.style['grid-row-gap'] = value From 7e9cbb69dc0ca5c2b81d31fb7301920f73512bf0 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 14 Oct 2020 15:51:55 +0200 Subject: [PATCH 003/110] Update README.md Live play not available because of recent repl.it updates. I will look for a different live test solution. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 1e84d728..5e7a7984 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,7 @@ Do you need support? There is also a **drag n drop GUI Editor**. Look at the [Editor](https://github.com/dddomodossola/remi/tree/master/editor) subfolder to download your copy.

-** Live Play Graphical GUI Editor ** -- For a comfortable use download it -

+ Changelog === From 0b286d10321e276b6b1b68c90caa874a11652147 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 20 Oct 2020 14:46:10 +0200 Subject: [PATCH 004/110] BugFix #412 --- remi/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 11b31e2e..09ca88be 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -22,7 +22,7 @@ try: import html escape = html.escape -except ImportError: +except ImportError : import cgi escape = cgi.escape import mimetypes @@ -38,7 +38,7 @@ from html.parser import HTMLParser h = HTMLParser() unescape = h.unescape - except ImportError: + except (ImportError, AttributeError): # Python 3.4+ import html unescape = html.unescape From da6cefbc7205a8ecbc4d0cb59f301ccb19b6a9a3 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 22 Oct 2020 11:46:55 +0200 Subject: [PATCH 005/110] FileFolderNavigator is now a widget in the editor. --- editor/editor_widgets.py | 2 + remi/gui.py | 86 +++++++++++++++++++++++++++------------- test/test_widget.py | 2 +- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index a2c16609..d59a523e 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -609,6 +609,8 @@ def __init__(self, appInstance, **kwargs): 'top': '20px', 'left': '20px', 'position': 'absolute'}) self.add_widget_to_collection(gui.Progress, value=0, _max=100, width='130px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + self.add_widget_to_collection(gui.FileFolderNavigator, width=100, height=100, style = { + 'top': '20px', 'left': '20px', 'position': 'absolute'}) #self.add_widget_to_collection(gui.VideoPlayer, width='100px', height='100px', style={ # 'top': '20px', 'left': '20px', 'position': 'absolute'}) self.add_widget_to_collection(gui.TableWidget, width='100px', height='100px', style={ diff --git a/remi/gui.py b/remi/gui.py index 857f112c..43d9301b 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3640,47 +3640,65 @@ def __init__(self, filename, **kwargs): self.attributes['data'] = filename -class FileFolderNavigator(Container): +class FileFolderNavigator(GridBox): """FileFolderNavigator widget.""" - def __init__(self, multiple_selection, selection_folder, allow_file_selection, allow_folder_selection, **kwargs): + @property + @editor_attribute_decorator("WidgetSpecific", '''Defines wether it is possible to select multiple items.''', bool, {}) + def multiple_selection(self): return self._multiple_selection + @multiple_selection.setter + def multiple_selection(self, value): self._multiple_selection = value + + @property + @editor_attribute_decorator("WidgetSpecific", '''Defines the actual navigator location.''', str, {}) + def selection_folder(self): return self._selection_folder + @selection_folder.setter + def selection_folder(self, value): + # fixme: we should use full paths and not all this chdir stuff + self.chdir(value) # move to actual working directory + + @property + @editor_attribute_decorator("WidgetSpecific", '''Defines if files are selectable.''', bool, {}) + def allow_file_selection(self): return self._allow_file_selection + @allow_file_selection.setter + def allow_file_selection(self, value): self._allow_file_selection = value + + @property + @editor_attribute_decorator("WidgetSpecific", '''Defines if folders are selectable.''', bool, {}) + def allow_folder_selection(self): return self._allow_folder_selection + @allow_folder_selection.setter + def allow_folder_selection(self, value): self._allow_folder_selection = value + + def __init__(self, multiple_selection = False, selection_folder = ".", allow_file_selection = True, allow_folder_selection = False, **kwargs): super(FileFolderNavigator, self).__init__(**kwargs) - self.set_layout_orientation(Container.LAYOUT_VERTICAL) - self.style['width'] = '100%' + + self.css_grid_template_columns = "30px auto 30px" + self.css_grid_template_rows = "30px auto" + self.define_grid([('button_back','url_editor','button_go'), ('items','items','items')]) self.multiple_selection = multiple_selection self.allow_file_selection = allow_file_selection self.allow_folder_selection = allow_folder_selection self.selectionlist = [] self.currDir = '' - self.controlsContainer = Container() - self.controlsContainer.set_size('100%', '30px') - self.controlsContainer.css_display = 'flex' - self.controlsContainer.set_layout_orientation(Container.LAYOUT_HORIZONTAL) self.controlBack = Button('Up') - self.controlBack.set_size('10%', '100%') self.controlBack.onclick.connect(self.dir_go_back) self.controlGo = Button('Go >>') - self.controlGo.set_size('10%', '100%') self.controlGo.onclick.connect(self.dir_go) self.pathEditor = TextInput() - self.pathEditor.set_size('80%', '100%') self.pathEditor.style['resize'] = 'none' self.pathEditor.attributes['rows'] = '1' - self.controlsContainer.append(self.controlBack, "button_back") - self.controlsContainer.append(self.pathEditor, "url_editor") - self.controlsContainer.append(self.controlGo, "button_go") + self.append(self.controlBack, "button_back") + self.append(self.pathEditor, "url_editor") + self.append(self.controlGo, "button_go") - self.itemContainer = Container(width='100%', height=300) + self.itemContainer = Container(width='100%', height='100%') - self.append(self.controlsContainer, "controls_container") self.append(self.itemContainer, key='items') # defined key as this is replaced later self.folderItems = list() - # fixme: we should use full paths and not all this chdir stuff - self.chdir(selection_folder) # move to actual working directory - self._last_valid_path = selection_folder + self.selection_folder = selection_folder def get_selection_list(self): if self.allow_folder_selection and not self.selectionlist: @@ -3717,16 +3735,15 @@ def _sort_files(a, b): # this speeds up the navigation self.remove_child(self.itemContainer) # creation of a new instance of a itemContainer - self.itemContainer = Container(width='100%', height=300) - self.itemContainer.set_layout_orientation(Container.LAYOUT_VERTICAL) - self.itemContainer.style.update({'overflow-y': 'scroll', 'overflow-x': 'hidden', 'display': 'block'}) + self.itemContainer = Container(width='100%', height='100%') + self.itemContainer.style.update({'overflow-y': 'scroll', 'overflow-x': 'hidden'}) for i in l: full_path = os.path.join(directory, i) is_folder = not os.path.isfile(full_path) if (not is_folder) and (not self.allow_file_selection): continue - fi = FileFolderItem(i, is_folder) + fi = FileFolderItem(full_path, i, is_folder) fi.onclick.connect(self.on_folder_item_click) # navigation purpose fi.onselection.connect(self.on_folder_item_selected) # selection purpose self.folderItems.append(fi) @@ -3756,6 +3773,7 @@ def dir_go(self, widget): os.chdir(curpath) # restore the path def chdir(self, directory): + self._selection_folder = directory curpath = os.getcwd() # backup the path log.debug("FileFolderNavigator - chdir: %s" % directory) for c in self.folderItems: @@ -3771,11 +3789,17 @@ def chdir(self, directory): self.currDir = directory os.chdir(curpath) # restore the path + @decorate_set_on_listener("(self, emitter, selected_item, selection_list)") + @decorate_event def on_folder_item_selected(self, folderitem): + """ This event occurs when an element in the list is selected + Returns the newly selected element of type FileFolderItem(or None if it was not selectable) + and the list of selected elements of type str. + """ if folderitem.isFolder and (not self.allow_folder_selection): folderitem.set_selected(False) self.on_folder_item_click(folderitem) - return + return (None, self.selectionlist, ) if not self.multiple_selection: self.selectionlist = [] @@ -3789,13 +3813,20 @@ def on_folder_item_selected(self, folderitem): self.selectionlist.remove(f) else: self.selectionlist.append(f) + return (folderitem, self.selectionlist, ) + @decorate_set_on_listener("(self, emitter, clicked_item)") + @decorate_event def on_folder_item_click(self, folderitem): + """ This event occurs when a folder element is clicked. + Returns the clicked element of type FileFolderItem. + """ log.debug("FileFolderNavigator - on_folder_item_dblclick") # when an item is clicked two time f = os.path.join(self.pathEditor.get_text(), folderitem.get_text()) if not os.path.isfile(f): self.chdir(f) + return (folderitem, ) def get_selected_filefolders(self): return self.selectionlist @@ -3803,9 +3834,10 @@ def get_selected_filefolders(self): class FileFolderItem(Container): """FileFolderItem widget for the FileFolderNavigator""" - - def __init__(self, text, is_folder=False, *args, **kwargs): + path_and_filename = None #the complete path and filename + def __init__(self, path_and_filename, text, is_folder=False, *args, **kwargs): super(FileFolderItem, self).__init__(*args, **kwargs) + self.path_and_filename = path_and_filename self.isFolder = is_folder self.icon = Widget(_class='FileFolderItemIcon') # the icon click activates the onselection event, that is propagates to registered listener @@ -3863,7 +3895,7 @@ def __init__(self, title='File dialog', message='Select files and folders', self.css_width = '475px' self.fileFolderNavigator = FileFolderNavigator(multiple_selection, selection_folder, allow_file_selection, - allow_folder_selection) + allow_folder_selection, width="100%", height="330px") self.add_field('fileFolderNavigator', self.fileFolderNavigator) self.confirm_dialog.connect(self.confirm_value) diff --git a/test/test_widget.py b/test/test_widget.py index fb706f36..00d32a56 100755 --- a/test/test_widget.py +++ b/test/test_widget.py @@ -256,7 +256,7 @@ def test_init(self): class TestFileFolderItem(unittest.TestCase): def test_init(self): - widget = gui.FileFolderItem('test file folder item') + widget = gui.FileFolderItem('full path', 'test file folder item') assertValidHTML(widget.repr()) class TestFileSelectionDialog(unittest.TestCase): From efe5f442426f24ba934a858837699e309e9c5bd2 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 22 Oct 2020 14:35:40 +0200 Subject: [PATCH 006/110] New widget SelectionInputWidget and related usage example. --- examples/selection_input_app.py | 37 +++++++++++++++++++ remi/gui.py | 64 ++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 examples/selection_input_app.py diff --git a/examples/selection_input_app.py b/examples/selection_input_app.py new file mode 100644 index 00000000..47fe2037 --- /dev/null +++ b/examples/selection_input_app.py @@ -0,0 +1,37 @@ +""" + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import remi.gui as gui +from remi import start, App +import os + + +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'}) + + label = gui.Label("Select a fruit") + + selection_input = gui.SelectionInputWidget(['banana', 'apple', 'pear', 'apricot'], 'banana', 'text') + selection_input.oninput.do(lambda emitter, value: label.set_text("event oninput: %s"%value)) + main_container.append([label, selection_input]) + + # returning the root widget + return main_container + + +if __name__ == "__main__": + # starts the webserver + start(MyApp, address='0.0.0.0', port=0, start_browser=True, username=None, password=None) diff --git a/remi/gui.py b/remi/gui.py index 43d9301b..e13e775e 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3514,10 +3514,12 @@ def __init__(self, default_value='2015-04-13', **kwargs): class Datalist(Container): - def __init__(self, *args, **kwargs): + def __init__(self, options=None, *args, **kwargs): super(Datalist, self).__init__(*args, **kwargs) self.type = 'datalist' self.css_display = 'none' + if options: + self.append(options) def append(self, options, key=''): if type(options) in (list, tuple, dict): @@ -3584,20 +3586,20 @@ def attr_input_type(self): return self.attributes.get('type', 'text') @attr_input_type.setter def attr_input_type(self, value): self.attributes['type'] = str(value) - def __init__(self, default_value="", input_type="text", **kwargs): + def __init__(self, default_value="", input_type="text", *args, **kwargs): """ Args: selection_type (str): text, search, url, tel, email, date, month, week, time, datetime-local, number, range, color. kwargs: See Widget.__init__() """ - super(SelectionInput, self).__init__(input_type, default_value, **kwargs) + super(SelectionInput, self).__init__(input_type, default_value, *args, **kwargs) self.attributes[Widget.EVENT_ONCHANGE] = \ "var params={};params['value']=document.getElementById('%(emitter_identifier)s').value;" \ "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);"% \ {'emitter_identifier':str(self.identifier), 'event_name':Widget.EVENT_ONCHANGE} - @decorate_set_on_listener("(self, emitter, x, y)") + @decorate_set_on_listener("(self, emitter, value)") @decorate_event_js("""var params={}; params['value']=this.value; remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);""") @@ -3624,6 +3626,60 @@ def get_datalist_identifier(self): return self.attr_datalist_identifier +class SelectionInputWidget(Container): + datalist = None #the internal Datalist + selection_input = None #the internal selection_input + + @property + @editor_attribute_decorator("WidgetSpecific", '''Defines the actual value for the widget.''', str, {}) + def attr_value(self): return self.selection_input.attr_value + @attr_value.setter + def attr_value(self, value): self.selection_input.attr_value = str(value) + + @property + @editor_attribute_decorator("WidgetSpecific", '''Defines the view type.''', 'DropDown', {'possible_values': ('text', 'search', 'url', 'tel', 'email', 'date', 'month', 'week', 'time', 'datetime-local', 'number', 'range', 'color')}) + def attr_input_type(self): return self.selection_input.attr_input_type + @attr_input_type.setter + def attr_input_type(self, value): self.selection_input.attr_input_type = str(value) + + def __init__(self, iterable_of_str=None, default_value="", input_type='text', *args, **kwargs): + super(SelectionInputWidget, self).__init__(*args, **kwargs) + options = None + if iterable_of_str: + options = list(map(DatalistItem, iterable_of_str)) + self.datalist = Datalist(options) + self.selection_input = SelectionInput(default_value, input_type, style={'top':'0px', + 'left':'0px', 'bottom':'0px', 'right':'0px'}) + self.selection_input.set_datalist_identifier(self.datalist.identifier) + self.append([self.datalist, self.selection_input]) + self.selection_input.oninput.do(self.oninput) + + def set_value(self, value): + """ + Sets the value of the widget + Args: + value (str): the string value + """ + self.attr_value = value + + def get_value(self): + """ + Returns: + str: the actual value + """ + return self.attr_value + + @decorate_set_on_listener("(self, emitter, value)") + @decorate_event + def oninput(self, emitter, value): + """ + This event occurs when user inputs a new value + Returns: + value (str): the string value + """ + return (value, ) + + class GenericObject(Widget): """ GenericObject widget - allows to show embedded object like pdf,swf.. From 3844dcbf1b5839de113eb6316f7d04b18595c2a0 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Sat, 24 Oct 2020 23:25:59 +0200 Subject: [PATCH 007/110] Removed margin in TabBox. --- remi/res/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/remi/res/style.css b/remi/res/style.css index dee733a7..2eafb50f 100644 --- a/remi/res/style.css +++ b/remi/res/style.css @@ -590,6 +590,7 @@ body.remi-main > div { .remi-main .TabBox ul { padding-left: 0; + margin: 0px; } .remi-main .TabBox ul li { From f311ea8bc9ca9f01a681ec6b8ddabfbf16896c91 Mon Sep 17 00:00:00 2001 From: Maxwell Morais Date: Tue, 27 Oct 2020 23:13:27 -0300 Subject: [PATCH 008/110] Remove duplicated import socket module is imported twice --- remi/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/remi/server.py b/remi/server.py index 060b7790..cb21f734 100644 --- a/remi/server.py +++ b/remi/server.py @@ -28,7 +28,6 @@ import mimetypes import webbrowser import struct -import socket import base64 import hashlib import sys From 6062c6f446f989d1527d588c326b64b8deebaf40 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 30 Oct 2020 11:25:44 +0100 Subject: [PATCH 009/110] New feature, query client properties of a widget. --- remi/gui.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/remi/gui.py b/remi/gui.py index e13e775e..b56016a4 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -1255,6 +1255,39 @@ def set_on_key_up_listener(self, callback, *userdata): def set_on_key_down_listener(self, callback, *userdata): self.onkeydown.connect(callback, *userdata) + def query_client(self, app_instance, attribute_list, style_property_list): + """ + WARNING: this is a new feature, subject to changes. + This method allows to query client rendering attributes and style properties of a widget. + The user, in order to get back the values must register a listener for the event 'onquery_client_result'. + Args: + app_instance (App): the app instance + attribute_list (list): list of attributes names + style_property_list (list): list of style property names + """ + app_instance.execute_javascript(""" + var params={}; + %(attributes)s + %(style)s + remi.sendCallbackParam('%(emitter_identifier)s','%(callback_name)s',params); + """ % { + 'attributes': ";".join(map(lambda param_name: "params['%(param_name)s']=document.getElementById('%(emitter_identifier)s').%(param_name)s" % {'param_name': param_name, 'emitter_identifier': str(self.identifier)}, attribute_list)), + 'style': ";".join(map(lambda param_name: "params['%(param_name)s']=document.getElementById('%(emitter_identifier)s').style.%(param_name)s" % {'param_name': param_name, 'emitter_identifier': str(self.identifier)}, style_property_list)), + 'emitter_identifier': str(self.identifier), + 'callback_name': 'onquery_client_result' + } + ) + + @decorate_set_on_listener("(self, emitter, values_dictionary)") + @decorate_event + def onquery_client_result(self, **kwargs): + """ WARNING: this is a new feature, subject to changes. + This event allows to get back the values fetched by 'query' method. + Returns: + values_dictionary (dict): a dictionary containing name:value of all the requested parameters + """ + return (kwargs,) + class Container(Widget): """ From b9ed9c4d47a5e0199f39834679a3fd2f60abf226 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 30 Oct 2020 11:40:39 +0100 Subject: [PATCH 010/110] Added missing statement in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 7df2bb56..69388b27 100644 --- a/setup.py +++ b/setup.py @@ -20,4 +20,5 @@ license="Apache", packages=setuptools.find_packages(), include_package_data=True, + setup_requires=['setuptools_scm'], ) From 06867b1499d5ee75e8d24e8584d46fd4b9e89780 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 30 Oct 2020 11:45:50 +0100 Subject: [PATCH 011/110] Switched to post-release version scheme, to avoid to get a false release date. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 69388b27..c34316a3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name="remi", description="Python REMote Interface library", - use_scm_version=True, + use_scm_version={'version_scheme': 'post-release'}, long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/dddomodossola/remi", From cc8acc25bab7c9b1a201e52163c2c34ab89f8905 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 30 Oct 2020 11:47:27 +0100 Subject: [PATCH 012/110] New example. It shows how query_client works. --- examples/query_attribute_app.py | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 examples/query_attribute_app.py diff --git a/examples/query_attribute_app.py b/examples/query_attribute_app.py new file mode 100644 index 00000000..0bb4aeb6 --- /dev/null +++ b/examples/query_attribute_app.py @@ -0,0 +1,47 @@ +""" + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import remi.gui as gui +from remi import start, App +import os + + +class MyApp(App): + def main(self): + # creating a container VBox type, vertical (you can use also HBox or Widget) + self.main_container = gui.Widget( + width="50%", height=200, style={'margin': '0px auto'}) + + # returning the root widget + return self.main_container + + def onpageshow(self, emitter, width, height): + """ WebPage Event that occurs on webpage gets shown """ + super(MyApp, self).onpageshow(emitter, width, height) + + attribute_list = [ + 'id', 'title', 'getBoundingClientRect().width', 'getBoundingClientRect().top'] + style_property_list = ['width', 'height'] + + # If a style property name is identical to an attribute name, call the query function twice + # properly specifing the result listener. + self.main_container.onquery_client_result.do( + lambda emitter, kwargs: print(str(kwargs))) + self.main_container.query_client(self, attribute_list, style_property_list) + + +if __name__ == "__main__": + # starts the webserver + start(MyApp, address='0.0.0.0', port=0, + start_browser=True, username=None, password=None) From 487dd24cf23f51911f6d784e323508683c2a7335 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 30 Oct 2020 17:30:08 +0100 Subject: [PATCH 013/110] Little fix in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e7a7984..89d2671c 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,7 @@ Projects using Remi [The Python Banyan Framework](https://github.com/MrYsLab/python_banyan) -[LightShowPi show manager](https://bitbucket.org/chrispizzi75/lightshowpishowmanager) +[LightShowPi show manager](https://github.com/Chrispizzi75/ShowManager) [rElectrum](https://github.com/emanuelelaface/rElectrum): A powerful promising Electrum wallet manager for safe transactions. From b10d700c46ca577cea3a33865ed75effa7c334c2 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 2 Nov 2020 10:22:33 +0100 Subject: [PATCH 014/110] Editor little adjustments. --- editor/editor_widgets.py | 9 ++++++--- editor/res/widget_CheckBoxLabel.png | Bin 1043 -> 710 bytes editor/res/widget_FileFolderNavigator.png | Bin 0 -> 1357 bytes editor/res/widget_FileUploader.png | Bin 0 -> 506 bytes editor/res/widget_Image.png | Bin 1360 -> 1361 bytes 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 editor/res/widget_FileFolderNavigator.png create mode 100644 editor/res/widget_FileUploader.png diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index d59a523e..b7b62ee3 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -463,8 +463,9 @@ def __init__(self, appInstance, widgetClass, **kwargs_to_widget): self.appInstance = appInstance self.widgetClass = widgetClass super(WidgetHelper, self).__init__() - self.style.update({'background-color': 'white', 'width': "60px", - "height": "60px", "justify-content": "center", "align-items": "center"}) + self.style.update({'background-color': 'rgb(250,250,250)', 'width': "auto", 'margin':"2px", + "height": "60px", "justify-content": "center", "align-items": "center", + 'font-size': '12px'}) if hasattr(widgetClass, "icon"): if type(widgetClass.icon) == gui.Svg: self.icon = widgetClass.icon @@ -484,7 +485,9 @@ def __init__(self, appInstance, widgetClass, **kwargs_to_widget): self.icon.style['image-rendering'] = 'auto' self.icon.attributes['draggable'] = 'false' self.icon.attributes['ondragstart'] = "event.preventDefault();" - self.append(self.icon) + self.append(self.icon, 'icon') + self.append(gui.Label(self.widgetClass.__name__), 'label') + self.children['label'].style.update({'margin-left':'2px', 'margin-right':'3px'}) self.attributes.update({'draggable': 'true', 'ondragstart': "this.style.cursor='move'; event.dataTransfer.dropEffect = 'move'; event.dataTransfer.setData('application/json', JSON.stringify(['add',event.target.id,(event.clientX),(event.clientY)]));", diff --git a/editor/res/widget_CheckBoxLabel.png b/editor/res/widget_CheckBoxLabel.png index 7c8d14c0078848194925ea98d3d23af58030d846..fe1ca49da7c2aedf3e5b7aa214555883e1c13322 100644 GIT binary patch delta 639 zcmV-_0)YLK2*w2=iBL{Q4GJ0x0000DNk~Le0000S0000S2nGNE0CElAl#wA+f5HF& z4#EKyC`y0;00J*bL_t(YOSP6g&%sa>$B&Yri-8r3!D{j;#A;;l4J;O406P=08+-!> z8!;jhL1Hs_5ApWof8929TeQ#9=a-zc_w?$or_OEbSg+R);#{v+ayT4(WFJnalkX8R z;pudu;c!Tg$Aj+oJ3nL(C;$P zlK&+GKA(>~9uFOlN0LykUAOUYIPg6^aKI(;@t^7SdX&v(snhB3j4@0I`(I)@ol>Pz zVNHiELY#wRFT&gH_N&5`K1J%rCe=}941agf= zL!F1BRN-$cY&M&OB_+xbSZ1kI>Sewv)F#aanxxfgv6F~+y7=9#P4>u=XwB9Wk2EXGHA-U@NzQmI7a z@tDfxvMNPo+6&MEQ8Jn2eVGd00VqUL_t(oN7YzANNQ0SR};#X9uzd0OJGYxP_(?&DMYX(nksQ>3R+xz zI5lWAz8Y?Y!`l=bnp~1{vS^6Niy$R51aGjcFzVI&oqKNA>-~GY_g&(BL+SDRe&_pr z|IWGB-E1~pg1o)Gx!rF1*_!nAfAv{2{8v_PkZHHuS65dbA0O}U@0M{92*||5#Ngnd zWe}T&gv{C5ncwdp9v-%db$@?ftJUV_=4|3vHut=~zQWi(6Mzg24GjzoJU>5M_Qs~6 z2eo0Xx}Kh%)O!QUX5-o6HFJlo2pNR4C#l!#J3BiVe;D`+k=tU? zAxBwYMKXVwrPXTf?(RN5KEiLv(a}*Nk-(w^z_P1DI)n@}5u%7xTwh;PCRFe>o6S-X zhW4v$e_PGn*`cG0&|7A5xg6%WoC!W-tS>Gu{%4s%(-#T_MM!-<-}3Tue}6x7X6)p| z#8#YUieRh1-rU^m@9(SEe^*vkkQw>9Vnft~DDvWJ0 z83$iFoo+N5R104Z4i3}@OE?^!o}ShZ*Bx9Axr(@C5ZPN86;<=GpRP@^#B;7#LTn)T zCF%0>he5$bJp=&=KZY(izPUcDPX?}MG8vxdx3;z@ALK+LCb}Ra=EPV2lrwyVY(fQ zVP_*0Majs7rc@gNf2urp5mL`X*AQgM8IJT)~X z;iF;DHkvniH2vDznn8#!FE3|iW^nkJrq>AtjnS-^AJ(RKf5J^ROzm?<&8LT^XWQ5b zE#2oC1s~R?cfvx>B{j@uksus+Z^m?Iv*xUs)nBoQ44CEgO-BF#002ovPDHLkV1fXc%boxL diff --git a/editor/res/widget_FileFolderNavigator.png b/editor/res/widget_FileFolderNavigator.png new file mode 100644 index 0000000000000000000000000000000000000000..b4f47c426a8749ff02e1f7436146091a40179475 GIT binary patch literal 1357 zcmV-T1+w~yP)qhdo4L``X_T4=k=?snVm?!0+F5AV&)PTfMczU1}SPG^6=&-?!V z%#IQfqBZ&B+qXqzFj&^;l>ie;1(Nu0!-{Vbga@Hz?FC9zR^+0+>bFn_0km8 zTYr-2>1SwUH@(>`Y#L5EYShYvL5r1I8KVPM+~1AbZCcI7TTdJ=d~hd#O!~40Jqvmk z&)depJv$f}9;IHIX70viR!Vd1$Y&Vn%fukPo3;Vazuj8d^$_@3!l1>^-<&uKbOeZ2 z8bBZxBwT=S9=tr8^CN8D`U0id8=U|46fZtK0?YGOLT96;02v&`8-9#NOW!GWCj|fz z;-m;*s-UajSJ-;*1M6~1vjSphZUjKY#%=pymstT5I+X;a)Eym*gfv@WmjEKRIE1tc zZYh_aKpQ+yk@3B?q9ex8Uf}Bj5I~ImZM6Va8hfJ92*wBsh3~JSgmu&%fQ=(@yckOW zOzhMcK`AgI_;dmgWJ3S~VpaphWGIscSW99NHV6;}mN*HiVu+lEM(~uv^VTXvTLWDK z*cxb8pbbJveB#guqB^M5?rCEwut8$S4nhQ!N}Osfc%C?^p;Je0JO!pLx2bkar@)9U zLJ`6C1EB0AY-^`30bCJE6fklIkW#?T0b{@jsN^Qcy1_1x_`}iZB?S;U)uG^t#7Za^ zT8XWVNSW>xz_wWOP)eqv*H(lT`uv{^lA2IVg)>Rl9VK=BCOY&mBvaCpFTAMup_voja9&@uzCm1&M%iZ zdHrO23bp|KKp&9p2(Yx+Jn-wK*~-FPJsC)CDJWHzSzKz-YK1nS7J_KU!6DGXCJp<& zT=Kob0_1@lpgRIQ_v%LzTAKscCreWc^L1MThWHe}T$z1ua;AK0=4Q42*R2}!w=Gbt zfKC+X)51Q-4~zY<1a6s20jITFBZo4 zwt{f1R&TxxjP>?p=x8eVFrSzOTRX#^?KhtE-*3`ws)p17n`2_U5ua<;qI=i&K}67tdV%8o1yffHhuD zJH2ca3*$$DH%pb}&yKuPJVIOk)>_5F_)cKwho4-SKl{V=GBCUPo+$3Wz$W?>>XX{) P00000NkvXXu0mjf9Cmr% literal 0 HcmV?d00001 diff --git a/editor/res/widget_FileUploader.png b/editor/res/widget_FileUploader.png new file mode 100644 index 0000000000000000000000000000000000000000..efffd6751466bd43eed6c7380d6ac9de0c7e95a1 GIT binary patch literal 506 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@)?VR+?^QK zos)S9l1z8>b!1@J*w6hZkrl}2EbxddW?X?_wfUrh8tOe=977`9lmGCu@Gx_1 zuzK+G^mU$m`pG;M&%QU!*|{L7u5V=ILyfN--Ex*$6=4Mfvkh6!yM&|7YqUlZ#Y(2 zH}p;wJ=Mhcr*FY7W)+rJ$r%zCdyl^5=*ijf{sgB+cg+X(858RN{9xQ7wzVTMEh+6o zVPV53g$A|EhStu`#`f;+#_8SCti^=|4xtJRy~2u#27hEfI9u|ZI>eXpiebXjoy)&| zv`YHn#(3ttLW90Y#v_K5(}@hzl~=rD$eDJS!K3DO@s+YeAH0}<)bKOESm>~I7lUox zM$42SiX3!;%e9)zA;2=>+E7F`2n2 zW1uIDv@M`OaP_Qu9$dS9(vnii={J^>?Fj?7@|1SNG|L+!XCAb=c z$5_uJax(%OZmjF=Ji|fp9tO9o%+Xg|2$noAS8^B4MLi4IN1gnKM)4gsx`Ool$MGIs z#O{(}hnPpH-!>kueUT|zjjJ(^g2SSnW_=4+wKJrMV~Pwq)!Zg)nIY#f zTi(fi@^Kbh&$|8t;^tL$Mc<;+Xr0g&0}#oE*dJTNvif&j*^eo>N#8)L{uO;9Qq-3g z*Xb2kxXf>yW+%t=O*~*Pc)+i~dDPyI&I9*dBe;SFr#|E6V=Z8dtU@1-I$D zIiY<`j*REv*Ti}DR=v+>)hkF7&$kB?jU0+Mkg|F``*g#_bPH!w`yB0x1P_Q5NA>k& zMBliB+gOCOW;R#9#3ikjjGaSFZX-u0Dg0rLvY5qRj8d?H#eg^7%bz2fCZB8Y3&_Bj) zGeYps`sc*NW&bnLfq6i!|E_N)mh0j`?1NGJM+olJPN>VI3Ybq>M4wOp$dyc6$-cZ` ziA`7(7wNt-Bzmfo;lxN=y&Q@+lO;;;Mk~MeeN)t)7i@~G z+6)Q_fa)`ROPT}|jYXwvUa;4~eZtfN)76H!mi?2%y&sKeD-dj{jeav4Fh_luw*DWm z3yP5ui&ojG0cNWWkua}e*uzz0f#6B{5MU(T%)S8a&Gi#hp%0cJ2L zybM%^U>)UGd5Fpo41-&g;%y2lNw7fqiVX#Vy`1eTqlUp%TOkF4E8q>j0A{HTp$-42 zkl7tTX$2CupY34YZ=H3=PYqmUdvm$oI-x+`E>E2cu`aryWU5I(Yt TR9kF800000NkvXXu0mjfhE{n) literal 1360 zcmV-W1+V&vP)Px#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vGi!vFvd!vV){sAK>D1i48>K~zXfwN}q>8$}eJnf0#M zYdiKjq;Z;Rb>WLP9+hi38$S z)c{3NOA!zy6-fw*nkJ4DI~&`(-u3M8W_E4wn%IY;jz+6p&(8O~`QG>58ZRy`qPsaT z5R8qD?MqQE@6N9qm*)$Au9XXhS*tfFZA1u8M5Tke_Rx|3Gx_Yq*l>^VmX?;f5|qp3 z@8$}h%&b%_+i6gkh`E?Cgj@s{#4uuq2NEwmHGcNNW8FA_fOK0k%*Ge9YpP5xoS%I8 ziDQ}~0U~f9;DD=38Kr-38lV2KuzI`D;}Bg5ET>VmoNOvOnM)ndrBxa5A#y-((J-J{ z-`*=#>UTnb5hfV@u~vHb%efD}UMTHZ0y==swSuF#%;l1G8(mKjkw{vL#3JO^<>EhE zdzrYR$K`l5;?r<+pRl(FJCnS6yJ$HSq@XS<7@(6YWbr`Q)teh%U0d-o!SNC6)0FpK zJUP~HAk;uea;0pa-u6|e8mMv1=A~&COkNqg3OwET*_O zs`Vf`1mT?nvpS+R)Tl_#yte!CUU9Cao1cT9|0-TtC@?UeOCfJ!a^riBY(DZ|TCsaV zk*0hIQVqKtO6(P-@@*%<%+0O3L%q$snwpxljV~k$qKl$qaaB&L1W~6cCikvvUhgDW z+Sv23_CfbzZ?ZOp1Xd@o@xBldbHfnnNyCkVz4wjekelOdC- zc$8nYXj)U#aaB*oj%4-c&Q3l%HKf+QK`v}>If5Z-w+MWjo_hTv<01HN;ba&eKa_d; zL@uXO8CMX31c(EO3)EmT7Rf|QO6~iI^*f@C5CPzv_|bEbPcZeyMW^AtBum5^?I!2%0mpxqVSZy%t$Nt zkWGWXdiTc8a$@HKp@oo8SYb>S#}b_i1R}|5_O0Z=#D5V&Hr;o5pOjJyQ?RVz%A-#F zG`9makPBaeuA}Q0t+}JaDx%tP%*?!C4!q`Qk8*U+8C^zK0!9*UWR!0X=UKF~x@_Kv z)P9rd%ZS7giQNf7Dt*98PE+YVgeA{Ufre~Hi=V~U-zId&(x+YdKo5dm|LnX}%eAYr zb%R)|gv$+Wk}#~gQa@G4?Zgz-@~IRQ(EbCO9fH4(;=^}W27vPu^vcoyUi%l7A{MmJ SbuHom0000 Date: Mon, 2 Nov 2020 16:26:40 +0100 Subject: [PATCH 015/110] Now editor widgets are appended by using their name as key. --- editor/editor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/editor/editor.py b/editor/editor.py index e6e86238..0a69db1d 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -925,7 +925,10 @@ def add_widget_to_editor(self, widget, parent=None, root_tree_node=True): self.configure_widget_for_editing(widget) #widget.identifier = widget.attributes.get('editor_varname', widget.identifier) - key = "root" if parent == self.project else widget.identifier + key = widget.identifier + if hasattr(widget, 'variable_name'): + key = widget.variable_name + key = "root" if parent == self.project else key if root_tree_node: parent.append(widget, key) if self.selectedWidget == self.project: From 2e16a8cb2b9ad49db50a1f24c82a14ab65964ce3 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 5 Nov 2020 23:36:34 +0100 Subject: [PATCH 016/110] Fix streange behaviour of SpinBox. Clicking + value (up), it was decreasing after the first click. Now seems to work correctly. --- remi/gui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/remi/gui.py b/remi/gui.py index b56016a4..7da013c8 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3456,15 +3456,21 @@ def onchange(self, value): _, _, _ = int(value), int(self.attributes['min']), int(self.attributes['max']) except: _type = float + _value = max(_type(value), _type(self.attributes['min'])) _value = min(_type(_value), _type(self.attributes['max'])) + self.attributes['value'] = str(_value) + #this is to force update in case a value out of limits arrived # and the limiting ended up with the same previous value stored in self.attributes # In this case the limitation gets not updated in browser # (because not triggering is_changed). So the update is forced. if _type(value) != _value: self.attributes.onchange() + else: + self.repr() + self._set_updated() return (_value, ) From e1c34a2f30967ee45393d2f8e2a0577be49bd3d5 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 6 Nov 2020 09:45:04 +0100 Subject: [PATCH 017/110] BugFix #415 --- setup.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/setup.py b/setup.py index c34316a3..4ae039c6 100644 --- a/setup.py +++ b/setup.py @@ -6,19 +6,26 @@ with open("README.md", "r") as fh: long_description = fh.read() -setup( - name="remi", - description="Python REMote Interface library", - use_scm_version={'version_scheme': 'post-release'}, - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/dddomodossola/remi", - download_url="https://github.com/dddomodossola/remi/archive/master.zip", - keywords=["gui-library", "remi", "platform-independent", "ui", "gui"], - author="Davide Rosa", - author_email="dddomodossola@gmail.com", - license="Apache", - packages=setuptools.find_packages(), - include_package_data=True, - setup_requires=['setuptools_scm'], -) +params = { + 'name':"remi", + 'description':"Python REMote Interface library", + 'use_scm_version':{'version_scheme': 'post-release'}, + 'long_description':long_description, + 'long_description_content_type':"text/markdown", + 'url':"https://github.com/dddomodossola/remi", + 'download_url':"https://github.com/dddomodossola/remi/archive/master.zip", + 'keywords':["gui-library", "remi", "platform-independent", "ui", "gui"], + 'author':"Davide Rosa", + 'author_email':"dddomodossola@gmail.com", + 'license':"Apache", + 'packages':setuptools.find_packages(), + 'include_package_data':True, + 'setup_requires':['setuptools_scm'], +} +try: + setup(**params) +except: + del params['setup_requires'] + params['use_scm_version'] = False + params['version'] = '2020.10.30' + setup(**params) From 0cc09c20d802548990b72a75cfc3a4cfecec0c24 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Sun, 8 Nov 2020 00:28:06 +0100 Subject: [PATCH 018/110] Additional methods to widget, to prevent clients update. This allows to refresh an input widget during client editing, because this causes interaction problems. --- remi/gui.py | 41 +++++++++++++++++++++++++++-------------- remi/server.py | 10 +++++++++- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 7da013c8..b8baf1ca 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -313,6 +313,7 @@ def __init__(self, attributes=None, _type='', _class=None, **kwargs): self.style = _EventDictionary() # used by Widget, but instantiated here to make gui_updater simpler self.ignore_update = False + self.refresh_enabled = True self.children.onchange.connect(self._need_update) self.attributes.onchange.connect(self._need_update) self.style.onchange.connect(self._need_update) @@ -389,7 +390,7 @@ def repr(self, changed_widgets=None): changed_widgets.update(local_changed_widgets) return self._backup_repr - def _need_update(self, emitter=None): + def _need_update(self, emitter=None, child_ignore_update=False): # if there is an emitter, it means self is the actual changed widget if not emitter is None: tmp = dict(self.attributes) @@ -399,10 +400,9 @@ def _need_update(self, emitter=None): tmp.pop('style', None) self._repr_attributes = ' '.join('%s="%s"' % (k, v) if v is not None else k for k, v in tmp.items()) - - if not self.ignore_update: + if self.refresh_enabled: if self.get_parent(): - self.get_parent()._need_update() + self.get_parent()._need_update(child_ignore_update = (self.ignore_update or child_ignore_update)) def _ischanged(self): return self.children.ischanged() or self.attributes.ischanged() or self.style.ischanged() @@ -413,9 +413,23 @@ def _set_updated(self): self.style.align_version() def disable_refresh(self): - self.ignore_update = True + """ Prevents the parent widgets to be notified about an update. + This is required to improve performances in case of widgets updated + multiple times in a procedure. + """ + self.refresh_enabled = False def enable_refresh(self): + self.refresh_enabled = True + + def disable_update(self): + """ Prevents clients updates. Remi will not send websockets update messages. + The widgets are however iternally updated. So if the user updates the + webpage, the update is shown. + """ + self.ignore_update = True + + def enable_update(self): self.ignore_update = False def add_class(self, cls): @@ -2287,9 +2301,9 @@ def onchange(self, new_value): Args: new_value (str): the new string content of the TextInput. """ - self.disable_refresh() + self.disable_update() self.set_value(new_value) - self.enable_refresh() + self.enable_update() return (new_value, ) @decorate_set_on_listener("(self, emitter, new_value, keycode)") @@ -2857,9 +2871,9 @@ def get_key(self): def onchange(self, value): """Called when a new DropDownItem gets selected. """ - self.disable_refresh() + self.disable_update() self.select_by_value(value) - self.enable_refresh() + self.enable_update() return (value, ) @decorate_explicit_alias_for_listener_registration @@ -3460,7 +3474,9 @@ def onchange(self, value): _value = max(_type(value), _type(self.attributes['min'])) _value = min(_type(_value), _type(self.attributes['max'])) + self.disable_update() self.attributes['value'] = str(_value) + self.enable_update() #this is to force update in case a value out of limits arrived # and the limiting ended up with the same previous value stored in self.attributes @@ -3468,9 +3484,6 @@ def onchange(self, value): # (because not triggering is_changed). So the update is forced. if _type(value) != _value: self.attributes.onchange() - else: - self.repr() - self._set_updated() return (_value, ) @@ -3643,9 +3656,9 @@ def __init__(self, default_value="", input_type="text", *args, **kwargs): params['value']=this.value; remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);""") def oninput(self, value): - self.disable_refresh() + self.disable_update() self.set_value(value) - self.enable_refresh() + self.enable_update() return (value, ) def set_value(self, value): diff --git a/remi/server.py b/remi/server.py index cb21f734..d2bd0d4a 100644 --- a/remi/server.py +++ b/remi/server.py @@ -434,7 +434,15 @@ def idle(self): Useful to schedule tasks. """ pass - def _need_update(self, emitter=None): + def _need_update(self, emitter=None, child_ignore_update=False): + if child_ignore_update: + #the widgets tree is processed to make it available for a intentional + # client update and to reset the changed flags of changed widget. + # Otherwise it will be updated on next update cycle. + changed_widget_dict = {} + self.root.repr(changed_widget_dict) + return + if self.update_interval == 0: #no interval, immadiate update self.do_gui_update() From 6f182dbae3fca0bc1a72cd4ee4043f6b3cf4b94e Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Sun, 8 Nov 2020 23:25:39 +0100 Subject: [PATCH 019/110] Now SpinBox restores the last valid value after enetering null content. --- remi/gui.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index b8baf1ca..f11ce0cc 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3471,18 +3471,28 @@ def onchange(self, value): except: _type = float - _value = max(_type(value), _type(self.attributes['min'])) - _value = min(_type(_value), _type(self.attributes['max'])) - - self.disable_update() - self.attributes['value'] = str(_value) - self.enable_update() - - #this is to force update in case a value out of limits arrived - # and the limiting ended up with the same previous value stored in self.attributes - # In this case the limitation gets not updated in browser - # (because not triggering is_changed). So the update is forced. - if _type(value) != _value: + try: + _value = max(_type(value), _type(self.attributes['min'])) + _value = min(_type(_value), _type(self.attributes['max'])) + + self.disable_update() + self.attributes['value'] = str(_value) + self.enable_update() + + #this is to force update in case a value out of limits arrived + # and the limiting ended up with the same previous value stored in self.attributes + # In this case the limitation gets not updated in browser + # (because not triggering is_changed). So the update is forced. + if _type(value) != _value: + self.attributes.onchange() + except: + #if the value conversion fails the client gui is updated with its previous value + _type = int + try: + _, _, _ = int(self.attributes['value']), int(self.attributes['min']), int(self.attributes['max']) + except: + _type = float + _value = _type(self.attributes['value']) self.attributes.onchange() return (_value, ) From 368aadcaa7784f4e0fa8b1cfc9f720f2478dc26e Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 10 Nov 2020 09:33:39 +0100 Subject: [PATCH 020/110] Release 2020.11.10 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ae039c6..a4bf2264 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,5 @@ except: del params['setup_requires'] params['use_scm_version'] = False - params['version'] = '2020.10.30' + params['version'] = '2020.11.10' setup(**params) From c029bd8711a27866342715b7b0d1db091f73ab19 Mon Sep 17 00:00:00 2001 From: Kavindu Santhusa <63494497+Ksengine@users.noreply.github.com> Date: Wed, 11 Nov 2020 04:34:45 +0000 Subject: [PATCH 021/110] move changelog --- README.md | 59 ------------------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/README.md b/README.md index 89d2671c..42e79995 100644 --- a/README.md +++ b/README.md @@ -23,65 +23,6 @@ There is also a **drag n drop GUI Editor**. Look at the [Editor](https://github.

-Changelog -=== -*2019 December 26* - -Since now remi is adopting class properties to setup css style and html attributes to make the applicable properties explicit. -This means that for example, to change a widget background you can now do: - -```python - mywidget.css_background_color = 'green' -``` - -The old method to setup style and attributes is still accepted: - -```python - mywidget.style['background-color'] = 'green' -``` - - -*2019 November 21* - -Widget class has no more **append** method. This means it cannot be used as a Container. -Use the new class Container as a generic container instead. -This allows higher code consistency. - - -*2019 April 1* - -Event listener registration can now be done by the **do** instruction instead of **connect** (that stays available for compatibility reasons). -i.e. -```python -mybutton.onclick.do(myevent_listener) -``` - -*Older changes* - -The current branch includes improvements about resource files handling. -App constructor accepts **static_file_path** parameter. Its value have to be a dictionary, where elements represents named resources paths. - -i.e. -```python -super(MyApp, self).__init__(*args, static_file_path = {'my_resources':'./files/resources/', 'my_other_res':'./other/'}) -``` -To address a specific resource, the user have to specify the resource folder key, prepending it to the filename in the format **'/key:'** -i.e. -```python -my_widget.attributes['background-image'] = "url('/my_resources:image.png')" -``` -Subfolders are accepted, and so: -```python -my_widget.attributes['background-image'] = "url('/my_resources:subfolder/other_subfolder/image.png')" -``` - -The event TextInput.onenter is no longer supported. - -The events TextInput.onkeydown and TextInput.onkeyup are now different, and require a different listener format. There is an additional parameter keycode. - -The TextInput.onchange event now occurs also in case of Enter key pressed, if TextInput is single_line. - - Getting Started === For a **stable** version: From 4ccb06581a70c101807b740ef587065aa70b17ce Mon Sep 17 00:00:00 2001 From: Kavindu Santhusa <63494497+Ksengine@users.noreply.github.com> Date: Wed, 11 Nov 2020 04:37:08 +0000 Subject: [PATCH 022/110] Create changelog.md --- doc/changelog.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 doc/changelog.md diff --git a/doc/changelog.md b/doc/changelog.md new file mode 100644 index 00000000..ede24fb2 --- /dev/null +++ b/doc/changelog.md @@ -0,0 +1,58 @@ +Changelog +=== +*2019 December 26* + +Since now remi is adopting class properties to setup css style and html attributes to make the applicable properties explicit. +This means that for example, to change a widget background you can now do: + +```python + mywidget.css_background_color = 'green' +``` + +The old method to setup style and attributes is still accepted: + +```python + mywidget.style['background-color'] = 'green' +``` + + +*2019 November 21* + +Widget class has no more **append** method. This means it cannot be used as a Container. +Use the new class Container as a generic container instead. +This allows higher code consistency. + + +*2019 April 1* + +Event listener registration can now be done by the **do** instruction instead of **connect** (that stays available for compatibility reasons). +i.e. +```python +mybutton.onclick.do(myevent_listener) +``` + +*Older changes* + +The current branch includes improvements about resource files handling. +App constructor accepts **static_file_path** parameter. Its value have to be a dictionary, where elements represents named resources paths. + +i.e. +```python +super(MyApp, self).__init__(*args, static_file_path = {'my_resources':'./files/resources/', 'my_other_res':'./other/'}) +``` +To address a specific resource, the user have to specify the resource folder key, prepending it to the filename in the format **'/key:'** +i.e. +```python +my_widget.attributes['background-image'] = "url('/my_resources:image.png')" +``` +Subfolders are accepted, and so: +```python +my_widget.attributes['background-image'] = "url('/my_resources:subfolder/other_subfolder/image.png')" +``` + +The event TextInput.onenter is no longer supported. + +The events TextInput.onkeydown and TextInput.onkeyup are now different, and require a different listener format. There is an additional parameter keycode. + +The TextInput.onchange event now occurs also in case of Enter key pressed, if TextInput is single_line. + From e1b45647644ed197f84a556ff308adea7c76494d Mon Sep 17 00:00:00 2001 From: Kavindu Santhusa <63494497+Ksengine@users.noreply.github.com> Date: Thu, 12 Nov 2020 03:26:02 +0000 Subject: [PATCH 023/110] contributors add image of contributors --- README.md | 38 ++++---------------------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 42e79995..be8ae650 100644 --- a/README.md +++ b/README.md @@ -352,42 +352,12 @@ Contributors === Thank you for collaborating with us to make Remi better! -The real power of opensource is contributors. Please feel free to participate in this project, and consider to add yourself to the following list. +The real power of opensource is contributors. Please feel free to participate in this project, and consider to add yourself to the [contributors list](docs/contributors). Yes, I know that GitHub already provides a list of contributors, but I feel that I must mention who helps. -[Davide Rosa](https://github.com/dddomodossola) - -[John Stowers](https://github.com/nzjrs) - -[Claudio Cannatà](https://github.com/cyberpro4) - -[Sam Pfeiffer](https://github.com/awesomebytes) - -[Ken Thompson](https://github.com/KenT2) - -[Paarth Tandon](https://github.com/Paarthri) - -[Ally Weir](https://github.com/allyjweir) - -[Timothy Cyrus](https://github.com/tcyrus) - -[John Hunter Bowen](https://github.com/jhb188) - -[Martin Spasov](https://github.com/SuburbanFilth) - -[Wellington Castello](https://github.com/wcastello) - -[PURPORC](https://github.com/PURPORC) - -[ttufts](https://github.com/ttufts) - -[Chris Braun](https://github.com/cryzed) - -[Alan Yorinks](https://github.com/MrYsLab) - -[Bernhard E. Reiter](https://github.com/bernhardreiter) - -[saewoonam](https://github.com/saewoonam) + + + Projects using Remi From c9cb1842c276effab33ebcc2f71af9b443db35bf Mon Sep 17 00:00:00 2001 From: Kavindu Santhusa <63494497+Ksengine@users.noreply.github.com> Date: Thu, 12 Nov 2020 03:29:07 +0000 Subject: [PATCH 024/110] contributors --- doc/contributors.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 doc/contributors.md diff --git a/doc/contributors.md b/doc/contributors.md new file mode 100644 index 00000000..e27a56ad --- /dev/null +++ b/doc/contributors.md @@ -0,0 +1,35 @@ +[Davide Rosa](https://github.com/dddomodossola) + +[John Stowers](https://github.com/nzjrs) + +[Claudio Cannatà](https://github.com/cyberpro4) + +[Sam Pfeiffer](https://github.com/awesomebytes) + +[Ken Thompson](https://github.com/KenT2) + +[Paarth Tandon](https://github.com/Paarthri) + +[Ally Weir](https://github.com/allyjweir) + +[Timothy Cyrus](https://github.com/tcyrus) + +[John Hunter Bowen](https://github.com/jhb188) + +[Martin Spasov](https://github.com/SuburbanFilth) + +[Wellington Castello](https://github.com/wcastello) + +[PURPORC](https://github.com/PURPORC) + +[ttufts](https://github.com/ttufts) + +[Chris Braun](https://github.com/cryzed) + +[Alan Yorinks](https://github.com/MrYsLab) + +[Bernhard E. Reiter](https://github.com/bernhardreiter) + +[saewoonam](https://github.com/saewoonam) + +[Kavindu Santhusa](https://github.com/Ksengine) From 8db904362f013486a78cab061ad02c5d3b1a6dfa Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 19 Nov 2020 14:43:07 +0100 Subject: [PATCH 025/110] Some Editor improvements. --- editor/editor_widgets.py | 2 +- remi/gui.py | 88 ++++++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index b7b62ee3..2296174c 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -729,7 +729,7 @@ def __init__(self, appInstance, **kwargs): self.infoLabel.style['-webkit-order'] = '0' self.group_orders = { - 'Generic': '2', 'WidgetSpecific': '3', 'Geometry': '34', 'Background': '5'} + 'Generic': '2', 'WidgetSpecific': '3', 'Geometry': '4', 'Background': '5', 'Transformation': '6'} self.attributesInputs = list() # load editable attributes diff --git a/remi/gui.py b/remi/gui.py index f11ce0cc..b7809017 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3409,19 +3409,19 @@ class SpinBox(Input): """spin box widget useful as numeric input field implements the onchange event. """ @property - @editor_attribute_decorator("WidgetSpecific", '''Defines the actual value for the spin box.''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + @editor_attribute_decorator("WidgetSpecific", '''Defines the actual value for the spin box.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1}) def attr_value(self): return self.attributes.get('value', '0') @attr_value.setter def attr_value(self, value): self.attributes['value'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Defines the minimum value for the spin box.''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + @editor_attribute_decorator("WidgetSpecific", '''Defines the minimum value for the spin box.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1}) def attr_min(self): return self.attributes.get('min', '0') @attr_min.setter def attr_min(self, value): self.attributes['min'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Defines the maximum value for the spin box.''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + @editor_attribute_decorator("WidgetSpecific", '''Defines the maximum value for the spin box.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1}) def attr_max(self): return self.attributes.get('max', '65535') @attr_max.setter def attr_max(self, value): self.attributes['max'] = str(value) @@ -3501,19 +3501,19 @@ def onchange(self, value): class Slider(Input): @property - @editor_attribute_decorator("WidgetSpecific", '''Defines the actual value for the Slider.''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + @editor_attribute_decorator("WidgetSpecific", '''Defines the actual value for the Slider.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1}) def attr_value(self): return self.attributes.get('value', '0') @attr_value.setter def attr_value(self, value): self.attributes['value'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Defines the minimum value for the Slider.''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + @editor_attribute_decorator("WidgetSpecific", '''Defines the minimum value for the Slider.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1}) def attr_min(self): return self.attributes.get('min', '0') @attr_min.setter def attr_min(self, value): self.attributes['min'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Defines the maximum value for the Slider.''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + @editor_attribute_decorator("WidgetSpecific", '''Defines the maximum value for the Slider.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1}) def attr_max(self): return self.attributes.get('max', '65535') @attr_max.setter def attr_max(self, value): self.attributes['max'] = str(value) @@ -4335,6 +4335,32 @@ def set_stroke(self, width=1, color='black'): self.attr_stroke_width = str(width) +class _MixinTransformable(): + @property + @editor_attribute_decorator("Transformation", '''Transform commands (i.e. rotate(45), translate(30,100)).''', str, {}) + def css_transform(self): return self.style.get('transform', None) + @css_transform.setter + def css_transform(self, value): self.style['transform'] = str(value) + @css_transform.deleter + def css_transform(self): del self.style['transform'] + + @property + @editor_attribute_decorator("Transformation", '''Transform origin as percent or absolute x,y pair value or ['center','top','bottom','left','right'] .''', str, {}) + def css_transform_origin(self): return self.style.get('transform-origin', None) + @css_transform_origin.setter + def css_transform_origin(self, value): self.style['transform-origin'] = str(value) + @css_transform_origin.deleter + def css_transform_origin(self): del self.style['transform-origin'] + + @property + @editor_attribute_decorator("Transformation", '''Alters the behaviour of tranform and tranform-origin by defining the transform box.''', 'DropDown', {'possible_values': ('content-box','border-box','fill-box','stroke-box','view-box')}) + def css_transform_box(self): return self.style.get('transform-box', None) + @css_transform_box.setter + def css_transform_box(self, value): self.style['transform-box'] = str(value) + @css_transform_box.deleter + def css_transform_box(self): del self.style['transform-box'] + + class _MixinSvgFill(): @property @editor_attribute_decorator("WidgetSpecific", '''Fill color for svg elements.''', 'ColorPicker', {}) @@ -4363,13 +4389,13 @@ def set_fill(self, color='black'): class _MixinSvgPosition(): @property - @editor_attribute_decorator("WidgetSpecific", '''Coordinate for Svg element.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Coordinate for Svg element.''', float, {'possible_values': '', 'min': -65635.0, 'max': 65635.0, 'default': 1.0, 'step': 0.1}) def attr_x(self): return self.attributes.get('x', '0') @attr_x.setter def attr_x(self, value): self.attributes['x'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Coordinate for Svg element.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Coordinate for Svg element.''', float, {'possible_values': '', 'min': -65635.0, 'max': 65635.0, 'default': 1.0, 'step': 0.1}) def attr_y(self): return self.attributes.get('y', '0') @attr_y.setter def attr_y(self, value): self.attributes['y'] = str(value) @@ -4387,13 +4413,13 @@ def set_position(self, x, y): class _MixinSvgSize(): @property - @editor_attribute_decorator("WidgetSpecific", '''Width for Svg element.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Width for Svg element.''', float, {'possible_values': '', 'min': 0.0, 'max': 65635.0, 'default': 1.0, 'step': 0.1}) def attr_width(self): return self.attributes.get('width', '100') @attr_width.setter def attr_width(self, value): self.attributes['width'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Height for Svg element.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Height for Svg element.''', float, {'possible_values': '', 'min': 0.0, 'max': 65635.0, 'default': 1.0, 'step': 0.1}) def attr_height(self): return self.attributes.get('height', '100') @attr_height.setter def attr_height(self, value): self.attributes['height'] = str(value) @@ -4609,7 +4635,7 @@ def __init__(self, x=0, y=0, width=100, height=100, *args, **kwargs): _MixinSvgSize.set_size(self, width, height) -class SvgGroup(Container, _MixinSvgStroke, _MixinSvgFill): +class SvgGroup(Container, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): """svg group - a non visible container for svg widgets, this have to be appended into Svg elements.""" def __init__(self, *args, **kwargs): @@ -4617,7 +4643,7 @@ def __init__(self, *args, **kwargs): self.type = 'g' -class SvgRectangle(Widget, _MixinSvgPosition, _MixinSvgSize, _MixinSvgStroke, _MixinSvgFill): +class SvgRectangle(Widget, _MixinSvgPosition, _MixinSvgSize, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @property @editor_attribute_decorator("WidgetSpecific", '''Horizontal round corners value.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) @@ -4646,7 +4672,7 @@ def __init__(self, x=0, y=0, w=100, h=100, *args, **kwargs): self.type = 'rect' -class SvgImage(Widget, _MixinSvgPosition, _MixinSvgSize): +class SvgImage(Widget, _MixinSvgPosition, _MixinSvgSize, _MixinTransformable): """svg image - a raster image element for svg graphics, this have to be appended into Svg elements.""" @property @@ -4682,21 +4708,21 @@ def __init__(self, image_data='', x=0, y=0, w=100, h=100, *args, **kwargs): _MixinSvgSize.set_size(self, w, h) -class SvgCircle(Widget, _MixinSvgStroke, _MixinSvgFill): +class SvgCircle(Widget, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @property - @editor_attribute_decorator("WidgetSpecific", '''Center coordinate for SvgCircle.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Center coordinate for SvgCircle.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_cx(self): return self.attributes.get('cx', None) @attr_cx.setter def attr_cx(self, value): self.attributes['cx'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Center coordinate for SvgCircle.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Center coordinate for SvgCircle.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_cy(self): return self.attributes.get('cy', None) @attr_cy.setter def attr_cy(self, value): self.attributes['cy'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific",'''Radius of SvgCircle.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific",'''Radius of SvgCircle.''', float, {'possible_values': '', 'min': 0.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_r(self): return self.attributes.get('r', None) @attr_r.setter def attr_r(self, value): self.attributes['r'] = str(value) @@ -4733,15 +4759,15 @@ def set_position(self, x, y): self.attr_cy = str(y) -class SvgEllipse(Widget, _MixinSvgStroke, _MixinSvgFill): +class SvgEllipse(Widget, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @property - @editor_attribute_decorator("WidgetSpecific", '''Coordinate for SvgEllipse.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Coordinate for SvgEllipse.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_cx(self): return self.attributes.get('cx', None) @attr_cx.setter def attr_cx(self, value): self.attributes['cx'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific", '''Coordinate for SvgEllipse.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Coordinate for SvgEllipse.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_cy(self): return self.attributes.get('cy', None) @attr_cy.setter def attr_cy(self, value): self.attributes['cy'] = str(value) @@ -4753,7 +4779,7 @@ def attr_rx(self): return self.attributes.get('rx', None) def attr_rx(self, value): self.attributes['rx'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific",'''Radius of SvgEllipse.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific",'''Radius of SvgEllipse.''', float, {'possible_values': '', 'min': 0.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_ry(self): return self.attributes.get('ry', None) @attr_ry.setter def attr_ry(self, value): self.attributes['ry'] = str(value) @@ -4793,27 +4819,27 @@ def set_position(self, x, y): self.attr_cy = str(y) -class SvgLine(Widget, _MixinSvgStroke): +class SvgLine(Widget, _MixinSvgStroke, _MixinTransformable): @property - @editor_attribute_decorator("WidgetSpecific",'''P1 coordinate for SvgLine.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific",'''P1 coordinate for SvgLine.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_x1(self): return self.attributes.get('x1', None) @attr_x1.setter def attr_x1(self, value): self.attributes['x1'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific",'''P1 coordinate for SvgLine.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific",'''P1 coordinate for SvgLine.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_y1(self): return self.attributes.get('y1', None) @attr_y1.setter def attr_y1(self, value): self.attributes['y1'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific",'''P2 coordinate for SvgLine.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific",'''P2 coordinate for SvgLine.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_x2(self): return self.attributes.get('x2', None) @attr_x2.setter def attr_x2(self, value): self.attributes['x2'] = str(value) @property - @editor_attribute_decorator("WidgetSpecific",'''P2 coordinate for SvgLine.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific",'''P2 coordinate for SvgLine.''', float, {'possible_values': '', 'min': -65535.0, 'max': 65535.0, 'default': 1.0, 'step': 0.1}) def attr_y2(self): return self.attributes.get('y2', None) @attr_y2.setter def attr_y2(self, value): self.attributes['y2'] = str(value) @@ -4836,7 +4862,7 @@ def set_p2(self, x2, y2): self.attr_y2 = y2 -class SvgPolyline(Widget, _MixinSvgStroke, _MixinSvgFill): +class SvgPolyline(Widget, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @property @editor_attribute_decorator("WidgetSpecific",'''Defines the maximum values count.''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) def maxlen(self): return self.__maxlen @@ -4864,13 +4890,13 @@ def add_coord(self, x, y): self.attributes['points'] += "%s,%s " % (x, y) -class SvgPolygon(SvgPolyline, _MixinSvgStroke, _MixinSvgFill): +class SvgPolygon(SvgPolyline, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): def __init__(self, _maxlen=None, *args, **kwargs): super(SvgPolygon, self).__init__(_maxlen, *args, **kwargs) self.type = 'polygon' -class SvgText(Widget, _MixinSvgPosition, _MixinSvgStroke, _MixinSvgFill, _MixinTextualWidget): +class SvgText(Widget, _MixinSvgPosition, _MixinSvgStroke, _MixinSvgFill, _MixinTextualWidget, _MixinTransformable): @property @editor_attribute_decorator("WidgetSpecific", '''Length for svg text elements.''', int, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) @@ -4889,7 +4915,7 @@ def attr_lengthAdjust(self, value): self.attributes['lengthAdjust'] = str(value) def attr_lengthAdjust(self): del self.attributes['lengthAdjust'] @property - @editor_attribute_decorator("WidgetSpecific", '''Rotation angle for svg elements.''', float, {'possible_values': '', 'min': 0.0, 'max': 360.0, 'default': 1.0, 'step': 0.1}) + @editor_attribute_decorator("WidgetSpecific", '''Rotation angle for svg elements.''', float, {'possible_values': '', 'min': -360.0, 'max': 360.0, 'default': 1.0, 'step': 0.1}) def attr_rotate(self): return self.attributes.get('rotate', None) @attr_rotate.setter def attr_rotate(self, value): self.attributes['rotate'] = str(value) @@ -4919,7 +4945,7 @@ def __init__(self, x=10, y=10, text='svg text', *args, **kwargs): self.set_text(text) -class SvgPath(Widget, _MixinSvgStroke, _MixinSvgFill): +class SvgPath(Widget, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @property @editor_attribute_decorator("WidgetSpecific", '''Instructions for SvgPath.''', str, {}) def attr_d(self): return self.attributes.get('d', None) From f33fa4073925cde0df46af57789a6499c5413d26 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 20 Nov 2020 09:35:03 +0100 Subject: [PATCH 026/110] Version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a4bf2264..2d74e6ab 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,5 @@ except: del params['setup_requires'] params['use_scm_version'] = False - params['version'] = '2020.11.10' + params['version'] = '2020.11.20' setup(**params) From 6ea90da0abd087d3fc0fad6fd4f032be06c4b24e Mon Sep 17 00:00:00 2001 From: Yngve Levinsen Date: Tue, 8 Dec 2020 16:36:05 +0100 Subject: [PATCH 027/110] Update README.md fixed wrong link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be8ae650..76b7dbfa 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,7 @@ Contributors === Thank you for collaborating with us to make Remi better! -The real power of opensource is contributors. Please feel free to participate in this project, and consider to add yourself to the [contributors list](docs/contributors). +The real power of opensource is contributors. Please feel free to participate in this project, and consider to add yourself to the [contributors list](doc/contributors.md). Yes, I know that GitHub already provides a list of contributors, but I feel that I must mention who helps. From 34f94841e95561e1bbfcc75e5048fceb6d85877e Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 11 Dec 2020 11:02:46 +0100 Subject: [PATCH 028/110] New example. --- examples/threaded_start_app.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 examples/threaded_start_app.py diff --git a/examples/threaded_start_app.py b/examples/threaded_start_app.py new file mode 100644 index 00000000..a93eb699 --- /dev/null +++ b/examples/threaded_start_app.py @@ -0,0 +1,65 @@ +""" + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +""" + This example shows how to start the application as a thread, + without stopping the main thread. + A label is accessed from the main thread. + NOTE: + It is important to run the server with parameter multiple_instance=False +""" + +import remi +import remi.gui as gui +from remi import start, App, Server +import time + +#here will be stored the application instance once a client connects to. +global_app_instance = None + +class MyApp(App): + label = None + + def main(self): + global global_app_instance + global_app_instance = 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'}) + self.label = gui.Label("a label") + + main_container.append(self.label) + + # returning the root widget + return main_container + + +if __name__ == "__main__": + #create the server with parameter start=False to prevent server autostart + server = remi.Server(MyApp, start=False, address='0.0.0.0', port=0, \ + start_browser=True, multiple_instance=False) + #start the server programmatically + server.start() + + index = 0 + #loop the main thread + while True: + #checks that the app instance is created + if not global_app_instance is None: + #the update lock is important to sync the app thread + with global_app_instance.update_lock: + #set the label value + global_app_instance.label.set_text("%s"%index) + index = index + 1 + time.sleep(1) From d9529f7fc07abd5d8c1d7a5b96fccbd8b87d33c4 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Fri, 25 Dec 2020 08:48:50 +1100 Subject: [PATCH 029/110] docs: fix simple typo, expections -> exception There is a small typo in test/html_validator.py. Should read `exception` rather than `expections`. --- test/html_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/html_validator.py b/test/html_validator.py index 21264b36..4d25536e 100644 --- a/test/html_validator.py +++ b/test/html_validator.py @@ -14,5 +14,5 @@ def handle_starttag(self, tag, attrs): def assertValidHTML(text): h = SimpleParser() h.feed(text) - # throws expections if invalid. + # throws exception if invalid. return True \ No newline at end of file From ca8bfe4e1ce9e1c0169b2895bc9b9a2126cf92e5 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 9 Feb 2021 16:06:53 +0100 Subject: [PATCH 030/110] Little improvement. --- remi/gui.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index b7809017..1d79f03e 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -116,13 +116,15 @@ def __init__(self, *args, **kwargs): def setup_event_methods(self): for (method_name, method) in inspect.getmembers(self, predicate=inspect.ismethod): + if not hasattr(method, '__is_event'): + continue + _event_info = None if hasattr(method, "_event_info"): _event_info = method._event_info - if hasattr(method, '__is_event'): - e = ClassEventConnector(self, method_name, method) - setattr(self, method_name, e) + e = ClassEventConnector(self, method_name, method) + setattr(self, method_name, e) if _event_info: getattr(self, method_name)._event_info = _event_info From 35b74690acf12adb2bf6a3f13c05695393451db6 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 9 Feb 2021 16:22:10 +0100 Subject: [PATCH 031/110] Improved performances during widgets creation. --- remi/gui.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 1d79f03e..4b3cf4e9 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -115,9 +115,12 @@ def __init__(self, *args, **kwargs): self.setup_event_methods() def setup_event_methods(self): - for (method_name, method) in inspect.getmembers(self, predicate=inspect.ismethod): - if not hasattr(method, '__is_event'): - continue + def a_method_not_builtin(obj): + return (not inspect.isbuiltin(obj)) and inspect.ismethod(obj) and hasattr(obj, '__is_event') + for (method_name, method) in inspect.getmembers(self, predicate=a_method_not_builtin): + # this is implicit in predicate + #if not hasattr(method, '__is_event'): + # continue _event_info = None if hasattr(method, "_event_info"): From bb713677635ca01fef0786af51c29248237313e6 Mon Sep 17 00:00:00 2001 From: Robin <41514960+Robin-Castellani@users.noreply.github.com> Date: Thu, 11 Feb 2021 14:20:00 +0100 Subject: [PATCH 032/110] Doc: let remi package be discoverable by Sphinx Previously building the documentation using the makefile rose warnings: it was not able to import `remi` package. The problem was that the repository root folder was not in `sys.path`, thus it was not considered when `remi.rst` tried to import `remi`. Now all modules are correctly imported and Sphinx documentation is correctly built and populated with docstrings. --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index fa1218d1..0cd7fa40 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -20,7 +20,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ From a8e0b57485a8ac786f336c0b829a6dba7bf0b234 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 12 Feb 2021 09:14:38 +0100 Subject: [PATCH 033/110] Updated PIL example for better refresh. --- examples/pil_app.py | 53 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/examples/pil_app.py b/examples/pil_app.py index 1ea19f2c..dfb91662 100644 --- a/examples/pil_app.py +++ b/examples/pil_app.py @@ -19,29 +19,62 @@ import remi.gui as gui from remi import start, App +import remi class PILImageViewverWidget(gui.Image): - def __init__(self, pil_image=None, **kwargs): + def __init__(self, filename=None, **kwargs): + self.app_instance = None super(PILImageViewverWidget, self).__init__("/res:logo.png", **kwargs) + self.frame_index = 0 self._buf = None + if filename: + self.load(filename) def load(self, file_path_name): pil_image = PIL.Image.open(file_path_name) self._buf = io.BytesIO() pil_image.save(self._buf, format='png') - self.refresh() - def refresh(self): - i = int(time.time() * 1e6) - self.attributes['src'] = "/%s/get_image_data?update_index=%d" % (id(self), i) + self.refresh() - def get_image_data(self, update_index): - if self._buf is None: + def search_app_instance(self, node): + if issubclass(node.__class__, remi.server.App): + return node + if not hasattr(node, "get_parent"): return None - self._buf.seek(0) - headers = {'Content-type': 'image/png'} - return [self._buf.read(), headers] + return self.search_app_instance(node.get_parent()) + + def refresh(self, *args): + if self.app_instance==None: + self.app_instance = self.search_app_instance(self) + if self.app_instance==None: + return + self.frame_index = self.frame_index + 1 + self.app_instance.execute_javascript(""" + url = '/%(id)s/get_image_data?index=%(frame_index)s'; + + xhr = null; + xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'blob' + xhr.onload = function(e){ + urlCreator = window.URL || window.webkitURL; + urlCreator.revokeObjectURL(document.getElementById('%(id)s').src); + imageUrl = urlCreator.createObjectURL(this.response); + document.getElementById('%(id)s').src = imageUrl; + } + xhr.send(); + """ % {'id': id(self), 'frame_index':self.frame_index}) + + def get_image_data(self, index=0): + try: + self._buf.seek(0) + headers = {'Content-type': 'image/png'} + return [self._buf.read(), headers] + except: + print(traceback.format_exc()) + return None, None class MyApp(App): From 9529e3499c7d3c8cc31e7a5ccaf92d5b4b3ef3d4 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 12 Feb 2021 09:32:02 +0100 Subject: [PATCH 034/110] pil_app.py fix. --- examples/pil_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pil_app.py b/examples/pil_app.py index dfb91662..b8560e8f 100644 --- a/examples/pil_app.py +++ b/examples/pil_app.py @@ -14,7 +14,7 @@ import time import io - +import traceback import PIL.Image import remi.gui as gui From 8a7109c2a83bee0e7813642726bbe1bee142ce04 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 12 Feb 2021 17:53:50 +0100 Subject: [PATCH 035/110] Reducing complexity a little. --- remi/gui.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 4b3cf4e9..153a7974 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -127,11 +127,9 @@ def a_method_not_builtin(obj): _event_info = method._event_info e = ClassEventConnector(self, method_name, method) + e._event_info = _event_info setattr(self, method_name, e) - if _event_info: - getattr(self, method_name)._event_info = _event_info - class ClassEventConnector(object): """ This class allows to manage the events. Decorating a method with *decorate_event* decorator From f08a302f57547b92919247c1aad76e7f8667dbb4 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 11:40:09 +0100 Subject: [PATCH 036/110] Removed backward compatibility with set_on_ listeners registration functions. --- editor/editor.py | 2 +- .../multiscreen_en .py | 6 +- remi/gui.py | 184 +----------------- 3 files changed, 14 insertions(+), 178 deletions(-) diff --git a/editor/editor.py b/editor/editor.py index 0a69db1d..2ee3d029 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -769,7 +769,7 @@ def main(self): lbl = gui.Label("Snap grid", width=100) self.spin_grid_size = gui.SpinBox('15', '1', '100', width=50) - self.spin_grid_size.set_on_change_listener(self.on_snap_grid_size_change) + self.spin_grid_size.onchange.do(self.on_snap_grid_size_change) grid_size = gui.HBox(children=[lbl, self.spin_grid_size], style={ 'outline': '1px solid gray', 'margin': '2px', 'margin-left': '10px'}) diff --git a/examples/examples_from_contributors/multiscreen_en .py b/examples/examples_from_contributors/multiscreen_en .py index a0a770c9..7272e02f 100644 --- a/examples/examples_from_contributors/multiscreen_en .py +++ b/examples/examples_from_contributors/multiscreen_en .py @@ -155,8 +155,8 @@ def main(self): #Add the menuContainer to the baseContainer and define the listeners for the menu elements baseContainer.append(menuContainer,'menuContainer') - baseContainer.children['menuContainer'].children['btnScreen2'].set_on_click_listener(self.onclick_btnScreen2) - baseContainer.children['menuContainer'].children['btnScreen1'].set_on_click_listener(self.onclick_btnScreen1) + baseContainer.children['menuContainer'].children['btnScreen2'].onclick.do(self.onclick_btnScreen2) + baseContainer.children['menuContainer'].children['btnScreen1'].onclick.do(self.onclick_btnScreen1) #The contentContainer contentContainer = Container() @@ -189,7 +189,7 @@ def main(self): #Define the listeners for GUI elements which are contained in the content Widgets #We can't define it in the Widget classes because the listeners wouldn't have access to other GUI elements outside the Widget - self.screen2.children['btnsend'].set_on_click_listener(self.send_text_to_screen1) + self.screen2.children['btnsend'].onclick.do(self.send_text_to_screen1) #Add the contentContainer to the baseContainer baseContainer.append(contentContainer,'contentContainer') diff --git a/remi/gui.py b/remi/gui.py index 153a7974..d1ad90b3 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -137,19 +137,21 @@ class ClassEventConnector(object): by a ClassEventConnector. This class overloads the __call__ method, where the event method is called, and after that the listener method is called too. """ + userdata = None + kwuserdata = None + callback = None def __init__(self, event_source_instance, event_name, event_method_bound): self.event_source_instance = event_source_instance self.event_name = event_name self.event_method_bound = event_method_bound - self.callback = None - self.userdata = () - self.kwuserdata = {} self.connect = self.do # for compatibility reasons def do(self, callback, *userdata, **kwuserdata): """ The callback and userdata gets stored, and if there is some javascript to add the js code is appended as attribute for the event source """ + self.userdata = userdata + self.kwuserdata = kwuserdata if hasattr(self.event_method_bound, '_js_code'): js_stop_propagation = kwuserdata.pop('js_stop_propagation', False) @@ -160,16 +162,17 @@ def do(self, callback, *userdata, **kwuserdata): ("event.preventDefault();" if js_prevent_default else "") self.callback = callback - if userdata: - self.userdata = userdata - if kwuserdata: - self.kwuserdata = kwuserdata def __call__(self, *args, **kwargs): # here the event method gets called callback_params = self.event_method_bound(*args, **kwargs) if not self.callback: return callback_params + + if not self.userdata: + self.userdata = () + if not self.kwuserdata: + self.kwuserdata = {} if not callback_params: callback_params = self.userdata else: @@ -218,14 +221,6 @@ def add_annotation(method): return add_annotation -def decorate_explicit_alias_for_listener_registration(method): - method.__doc__ = """ Registers the listener - For backward compatibility - Suggested new dialect event.connect(callback, *userdata) - """ - return method - - def editor_attribute_decorator(group, description, _type, additional_data): def add_annotation(prop): setattr(prop, "editor_attributes", {'description': description, 'type': _type, 'group': group, 'additional_data': additional_data}) @@ -1200,77 +1195,6 @@ def onkeydown(self, key, keycode, ctrl, shift, alt): """ return (key, keycode, ctrl, shift, alt) - @decorate_explicit_alias_for_listener_registration - def set_on_focus_listener(self, callback, *userdata): - self.onfocus.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_blur_listener(self, callback, *userdata): - self.onblur.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_click_listener(self, callback, *userdata): - self.onclick.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_dblclick_listener(self, callback, *userdata): - self.ondblclick.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_contextmenu_listener(self, callback, *userdata): - self.oncontextmenu.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_mousedown_listener(self, callback, *userdata): - self.onmousedown.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_mouseup_listener(self, callback, *userdata): - self.onmouseup.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_mouseout_listener(self, callback, *userdata): - self.onmouseout.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_mouseleave_listener(self, callback, *userdata): - self.onmouseleave.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_mousemove_listener(self, callback, *userdata): - self.onmousemove.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_touchmove_listener(self, callback, *userdata): - self.ontouchmove.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_touchstart_listener(self, callback, *userdata): - self.ontouchstart.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_touchend_listener(self, callback, *userdata): - self.ontouchend.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_touchenter_listener(self, callback, *userdata): - self.ontouchenter.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_touchleave_listener(self, callback, *userdata): - self.ontouchleave.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_touchcancel_listener(self, callback, *userdata): - self.ontouchcancel.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_key_up_listener(self, callback, *userdata): - self.onkeyup.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_key_down_listener(self, callback, *userdata): - self.onkeydown.connect(callback, *userdata) def query_client(self, app_instance, attribute_list, style_property_list): """ @@ -2339,18 +2263,6 @@ def onkeydown(self, new_value, keycode): """ return (new_value, keycode) - @decorate_explicit_alias_for_listener_registration - def set_on_change_listener(self, callback, *userdata): - self.onchange.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_key_up_listener(self, callback, *userdata): - self.onkeyup.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_key_down_listener(self, callback, *userdata): - self.onkeydown.connect(callback, *userdata) - class Label(Widget, _MixinTextualWidget): """ Non editable text label widget. Set its content by means of set_text function, and retrieve its content with the @@ -2546,14 +2458,6 @@ def show(self, base_app_instance): def hide(self): self._base_app_instance.set_root_widget(self._old_root_widget) - @decorate_explicit_alias_for_listener_registration - def set_on_confirm_dialog_listener(self, callback, *userdata): - self.confirm_dialog.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_cancel_dialog_listener(self, callback, *userdata): - self.cancel_dialog.connect(callback, *userdata) - class InputDialog(GenericDialog): """Input Dialog widget. It can be used to query simple and short textual input to the user. @@ -2596,10 +2500,6 @@ def confirm_value(self, widget): """Event called pressing on OK button.""" return (self.inputText.get_text(),) - @decorate_explicit_alias_for_listener_registration - def set_on_confirm_value_listener(self, callback, *userdata): - self.confirm_value.connect(callback, *userdata) - class ListView(Container): """List widget it can contain ListItems. Add items to it by using the standard append(item, key) function or @@ -2735,10 +2635,6 @@ def select_by_value(self, value): self._selected_item = item self._selected_item.attributes['selected'] = True - @decorate_explicit_alias_for_listener_registration - def set_on_selection_listener(self, callback, *userdata): - self.onselection.connect(callback, *userdata) - class ListItem(Widget, _MixinTextualWidget): """List item widget for the ListView. @@ -2879,10 +2775,6 @@ def onchange(self, value): self.enable_update() return (value, ) - @decorate_explicit_alias_for_listener_registration - def set_on_change_listener(self, callback, *userdata): - self.onchange.connect(callback, *userdata) - class DropDownItem(Widget, _MixinTextualWidget): """item widget for the DropDown""" @@ -3018,10 +2910,6 @@ def append(self, value, key=''): def on_table_row_click(self, row, item): return (row, item) - @decorate_explicit_alias_for_listener_registration - def set_on_table_row_click_listener(self, callback, *userdata): - self.on_table_row_click.connect(callback, *userdata) - class TableWidget(Table): """ @@ -3170,10 +3058,6 @@ def on_item_changed(self, item, new_value, row, column): """ return (item, new_value, row, column) - @decorate_explicit_alias_for_listener_registration - def set_on_item_changed_listener(self, callback, *userdata): - self.on_item_changed.connect(callback, *userdata) - class TableRow(Container): """ @@ -3214,10 +3098,6 @@ def on_row_item_click(self, item): """ return (item, ) - @decorate_explicit_alias_for_listener_registration - def set_on_row_item_click_listener(self, callback, *userdata): - self.on_row_item_click.connect(callback, *userdata) - class TableEditableItem(Container, _MixinTextualWidget): """item widget for the TableRow.""" @@ -3242,10 +3122,6 @@ def __init__(self, text='', *args, **kwargs): def onchange(self, emitter, new_value): return (new_value, ) - @decorate_explicit_alias_for_listener_registration - def set_on_change_listener(self, callback, *userdata): - self.onchange.connect(callback, *userdata) - class TableItem(Container, _MixinTextualWidget): """item widget for the TableRow.""" @@ -3318,10 +3194,6 @@ def set_read_only(self, readonly): except KeyError: pass - @decorate_explicit_alias_for_listener_registration - def set_on_change_listener(self, callback, *userdata): - self.onchange.connect(callback, *userdata) - class CheckBoxLabel(HBox): @@ -3358,10 +3230,6 @@ def __init__(self, label='checkbox', checked=False, user_data='', **kwargs): def onchange(self, widget, value): return (value, ) - @decorate_explicit_alias_for_listener_registration - def set_on_change_listener(self, callback, *userdata): - self.onchange.connect(callback, *userdata) - def get_text(self): return self._label.get_text() @@ -3551,10 +3419,6 @@ def __init__(self, default_value=0, min=0, max=65535, step=1, **kwargs): def oninput(self, value): return (value, ) - @decorate_explicit_alias_for_listener_registration - def set_oninput_listener(self, callback, *userdata): - self.oninput.connect(callback, *userdata) - class ColorPicker(Input): @@ -3995,14 +3859,6 @@ def set_text(self, t): def get_text(self): return self.children['text'].get_text() - @decorate_explicit_alias_for_listener_registration - def set_on_click_listener(self, callback, *userdata): - self.onclick.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_selection_listener(self, callback, *userdata): - self.onselection.connect(callback, *userdata) - class FileSelectionDialog(GenericDialog): """file selection dialog, it opens a new webpage allows the OK/CANCEL functionality @@ -4030,10 +3886,6 @@ def confirm_value(self, widget): params = (self.fileFolderNavigator.get_selection_list(),) return params - @decorate_explicit_alias_for_listener_registration - def set_on_confirm_value_listener(self, callback, *userdata): - self.confirm_value.connect(callback, *userdata) - class MenuBar(Container): @@ -4189,18 +4041,6 @@ def ondata(self, filedata, filename): f.write(filedata) return (filedata, filename) - @decorate_explicit_alias_for_listener_registration - def set_on_success_listener(self, callback, *userdata): - self.onsuccess.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_failed_listener(self, callback, *userdata): - self.onfailed.connect(callback, *userdata) - - @decorate_explicit_alias_for_listener_registration - def set_on_data_listener(self, callback, *userdata): - self.ondata.connect(callback, *userdata) - class FileDownloader(Container, _MixinTextualWidget): """FileDownloader widget. Allows to start a file download.""" @@ -4305,10 +4145,6 @@ def onended(self): """Called when the media has been played and reached the end.""" return () - @decorate_explicit_alias_for_listener_registration - def set_on_ended_listener(self, callback, *userdata): - self.onended.connect(callback, *userdata) - class _MixinSvgStroke(): @property From fdddf85ba2919c689620e15f4d25dfa890293185 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 12:07:31 +0100 Subject: [PATCH 037/110] Better code. --- editor/editor.py | 5 +++-- remi/gui.py | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/editor/editor.py b/editor/editor.py index 2ee3d029..cc4ecb93 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -718,7 +718,8 @@ def idle(self): drag_helper.update_position() def main(self): - + import time + t= time.time() #custom css my_css_head = """ @@ -865,7 +866,7 @@ def main(self): self.projectPathFilename = '' self.editCuttedWidget = None # cut operation, contains the cutted tag - + print(">>>>>>>>>>>>startup time:", time.time()-t) # returning the root widget return self.mainContainer diff --git a/remi/gui.py b/remi/gui.py index d1ad90b3..ddc6fcb9 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -116,7 +116,7 @@ def __init__(self, *args, **kwargs): def setup_event_methods(self): def a_method_not_builtin(obj): - return (not inspect.isbuiltin(obj)) and inspect.ismethod(obj) and hasattr(obj, '__is_event') + return hasattr(obj, '__is_event') for (method_name, method) in inspect.getmembers(self, predicate=a_method_not_builtin): # this is implicit in predicate #if not hasattr(method, '__is_event'): @@ -213,9 +213,7 @@ def decorate_set_on_listener(prototype): """ # noinspection PyDictCreation,PyProtectedMember def add_annotation(method): - method._event_info = {} - method._event_info['name'] = method.__name__ - method._event_info['prototype'] = prototype + method._event_info = {'name':method.__name__, 'prototype':prototype} return method return add_annotation From 6dbe6c8c2f42ab065d2fb009fde75dad982df8d1 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 12:22:36 +0100 Subject: [PATCH 038/110] Better code. --- remi/gui.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index ddc6fcb9..c33b5fd7 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -229,10 +229,8 @@ def add_annotation(prop): class _EventDictionary(dict, EventSource): """This dictionary allows to be notified if its content is changed. """ - + changed = False def __init__(self, *args, **kwargs): - self.__version__ = 0 - self.__lastversion__ = 0 super(_EventDictionary, self).__init__(*args, **kwargs) EventSource.__init__(self, *args, **kwargs) @@ -269,16 +267,16 @@ def update(self, d): return ret def ischanged(self): - return self.__version__ != self.__lastversion__ + return self.changed def align_version(self): - self.__lastversion__ = self.__version__ + self.changed = False @decorate_event def onchange(self): """Called on content change. """ - self.__version__ += 1 + self.changed = True return () @@ -401,7 +399,7 @@ def _need_update(self, emitter=None, child_ignore_update=False): self.get_parent()._need_update(child_ignore_update = (self.ignore_update or child_ignore_update)) def _ischanged(self): - return self.children.ischanged() or self.attributes.ischanged() or self.style.ischanged() + return self.children.changed or self.attributes.changed or self.style.changed def _set_updated(self): self.children.align_version() From d479330cdc816dbab5151085d0759225ecc99e37 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 12:36:55 +0100 Subject: [PATCH 039/110] Possible BugFix #423. --- remi/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/remi/server.py b/remi/server.py index e05ac4cf..0ff2de66 100644 --- a/remi/server.py +++ b/remi/server.py @@ -123,11 +123,12 @@ class WebSocketsHandler(socketserver.StreamRequestHandler): magic = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' - def __init__(self, headers, *args, **kwargs): + def __init__(self, headers, request, client_address, server, *args, **kwargs): self.headers = headers + self.server = server self.handshake_done = False self._log = logging.getLogger('remi.server.ws') - socketserver.StreamRequestHandler.__init__(self, *args, **kwargs) + socketserver.StreamRequestHandler.__init__(self, request, client_address, server, *args, **kwargs) def setup(self): socketserver.StreamRequestHandler.setup(self) @@ -200,7 +201,7 @@ def send_message(self, message): message = message.encode('utf-8') out = out + message - readable, writable, errors = select.select([], [self.request,], [], 0) #last parameter is timeout, when 0 is non blocking + readable, writable, errors = select.select([], [self.request,], [], self.server.websocket_timeout_timer_ms) #last parameter is timeout, when 0 is non blocking #self._log.debug('socket status readable=%s writable=%s errors=%s'%((self.request in readable), (self.request in writable), (self.request in error$ writable = self.request in writable if not writable: From 639b1b8979fda637ba70d1ad842c397c975f8a21 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 15:42:53 +0100 Subject: [PATCH 040/110] OpencvToolbox template matching widget. --- editor/widgets/__init__.py | 3 +- editor/widgets/toolbox_opencv.py | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/editor/widgets/__init__.py b/editor/widgets/__init__.py index 7d7304d9..224abb62 100644 --- a/editor/widgets/__init__.py +++ b/editor/widgets/__init__.py @@ -30,7 +30,8 @@ def __init__(self, msg="In order to use EPICS widgets install pyepics \n" + trac try: from .toolbox_opencv import OpencvImRead, OpencvCrop, OpencvVideo, OpencvThreshold, OpencvSplit, OpencvCvtColor, OpencvAddWeighted, OpencvBitwiseNot, OpencvBitwiseAnd, OpencvBitwiseOr,\ - OpencvBilateralFilter, OpencvBlurFilter, OpencvDilateFilter, OpencvErodeFilter, OpencvLaplacianFilter, OpencvCanny, OpencvFindContours, OpencvInRangeGrayscale + OpencvBilateralFilter, OpencvBlurFilter, OpencvDilateFilter, OpencvErodeFilter, OpencvLaplacianFilter, OpencvCanny, OpencvFindContours, OpencvInRangeGrayscale,\ + OpencvMatchTemplate except ImportError: class OPENCVPlaceholder(gui.Label): icon = default_icon("missing OPENCV") diff --git a/editor/widgets/toolbox_opencv.py b/editor/widgets/toolbox_opencv.py index 3e464bd7..0b36c3dc 100644 --- a/editor/widgets/toolbox_opencv.py +++ b/editor/widgets/toolbox_opencv.py @@ -1048,6 +1048,63 @@ def on_new_contours_result(self): return (self.contours, self.hierarchy) +class OpencvMatchTemplate(OpencvImage): + """ OpencvMatchTemplate widget. + Receives an image on on_new_image_listener and a template on on_template_listener. + Returns the template matching position on on_matching_success. + The event on_new_image can be connected to other Opencv widgets for further processing + """ + icon = None + + matching_methods = {'TM_CCOEFF':cv2.TM_CCOEFF, 'TM_CCOEFF_NORMED':cv2.TM_CCOEFF_NORMED, 'TM_CCORR':cv2.TM_CCORR, 'TM_CCORR_NORMED':cv2.TM_CCORR_NORMED, 'TM_SQDIFF':cv2.TM_SQDIFF, 'TM_SQDIFF_NORMED':cv2.TM_SQDIFF_NORMED} + + @property + @gui.editor_attribute_decorator('WidgetSpecific','The template matching method', 'DropDown', {'possible_values': matching_methods.keys()}) + def matching_method(self): + return self.__matching_method + @matching_method.setter + def matching_method(self, v): + self.__matching_method = v + self.on_new_image_listener(self.image_source) + + template_source = None + + def __init__(self, method=cv2.TM_CCORR_NORMED, *args, **kwargs): + super(OpencvMatchTemplate, self).__init__("", *args, **kwargs) + self.__matching_method = method + + def on_new_image_listener(self, emitter): + try: + self.image_source = emitter + method = OpencvMatchTemplate.matching_methods[self.matching_method] if type(self.matching_method) == str else self.matching_method + res = cv2.matchTemplate(emitter.img, self.template_source.img, method) + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) + top_left = max_loc + w, h = self.template_source.img.shape[::-1] + # If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum + if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]: + top_left = min_loc + bottom_right = (top_left[0] + w, top_left[1] + h) + + img = emitter.img.copy() + cv2.rectangle(img, top_left, bottom_right, 255, 2) + + self.set_image_data(img) + + self.on_matching_success(top_left, bottom_right) + except Exception: + print(traceback.format_exc()) + + def on_template_listener(self, emitter): + self.template_source = emitter + if hasattr(self, "image_source"): + self.on_new_image_listener(self.image_source) + + @gui.decorate_event + def on_matching_success(self, top_left, bottom_right): + return (top_left, bottom_right) + + class OpencvInRangeGrayscale(OpencvImage): """ OpencvInRangeGrayscale thresholding widget. Receives an image on on_new_image_listener. From 1fe08e19a63c9442ebf0af1a448c40275e4694c9 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 16:48:17 +0100 Subject: [PATCH 041/110] Toolbox opencv template matching correction. --- editor/widgets/toolbox_opencv.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/editor/widgets/toolbox_opencv.py b/editor/widgets/toolbox_opencv.py index 0b36c3dc..75f09293 100644 --- a/editor/widgets/toolbox_opencv.py +++ b/editor/widgets/toolbox_opencv.py @@ -1051,7 +1051,7 @@ def on_new_contours_result(self): class OpencvMatchTemplate(OpencvImage): """ OpencvMatchTemplate widget. Receives an image on on_new_image_listener and a template on on_template_listener. - Returns the template matching position on on_matching_success. + Returns the template matching position and size of matching rectangle on on_matching_success. The event on_new_image can be connected to other Opencv widgets for further processing """ icon = None @@ -1091,7 +1091,7 @@ def on_new_image_listener(self, emitter): self.set_image_data(img) - self.on_matching_success(top_left, bottom_right) + self.on_matching_success(top_left[0], top_left[1], w, h) except Exception: print(traceback.format_exc()) @@ -1100,9 +1100,10 @@ def on_template_listener(self, emitter): if hasattr(self, "image_source"): self.on_new_image_listener(self.image_source) + @gui.decorate_set_on_listener("(self, emitter, x, y, w, h)") @gui.decorate_event - def on_matching_success(self, top_left, bottom_right): - return (top_left, bottom_right) + def on_matching_success(self, x, y, w, h): + return (x, y, w, h) class OpencvInRangeGrayscale(OpencvImage): From 12b73dd52126ad5771f03acaa289875d700ee7c2 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 15 Feb 2021 17:33:02 +0100 Subject: [PATCH 042/110] Toolbox opencv template matching. --- editor/widgets/toolbox_opencv.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/editor/widgets/toolbox_opencv.py b/editor/widgets/toolbox_opencv.py index 75f09293..3cd374a6 100644 --- a/editor/widgets/toolbox_opencv.py +++ b/editor/widgets/toolbox_opencv.py @@ -449,7 +449,7 @@ class OpencvCvtColor(OpencvImage): """ icon = "data:image/png;base64,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" - cvt_types = {'COLOR_BGR2HSV':cv2.COLOR_BGR2HSV,'COLOR_HSV2BGR':cv2.COLOR_HSV2BGR, 'COLOR_RGB2BGR':cv2.COLOR_RGB2BGR, 'COLOR_RGB2GRAY':cv2.COLOR_RGB2GRAY, 'COLOR_BGR2GRAY':cv2.COLOR_BGR2GRAY, 'COLOR_RGB2HSV':cv2.COLOR_RGB2HSV} + cvt_types = {'COLOR_BGR2HSV':cv2.COLOR_BGR2HSV,'COLOR_HSV2BGR':cv2.COLOR_HSV2BGR, 'COLOR_RGB2BGR':cv2.COLOR_RGB2BGR, 'COLOR_RGB2GRAY':cv2.COLOR_RGB2GRAY, 'COLOR_BGR2GRAY':cv2.COLOR_BGR2GRAY, 'COLOR_RGB2HSV':cv2.COLOR_RGB2HSV, 'COLOR_GRAY2BGR':cv2.COLOR_GRAY2BGR, 'COLOR_GRAY2RGB':cv2.COLOR_GRAY2RGB} @property @gui.editor_attribute_decorator('WidgetSpecific','The conversion constant code', 'DropDown', {'possible_values': cvt_types.keys()}) def conversion_code(self): @@ -1067,17 +1067,28 @@ def matching_method(self, v): self.__matching_method = v self.on_new_image_listener(self.image_source) + @property + @gui.editor_attribute_decorator('WidgetSpecific','When true, the source image is shown with a rectangle in case of matching.', bool, {}) + def show_result_rectangle(self): + return self.__show_result_rectangle + @show_result_rectangle.setter + def show_result_rectangle(self, v): + self.__show_result_rectangle = v + self.on_new_image_listener(self.image_source) + template_source = None def __init__(self, method=cv2.TM_CCORR_NORMED, *args, **kwargs): super(OpencvMatchTemplate, self).__init__("", *args, **kwargs) self.__matching_method = method + self.__show_result_rectangle = True def on_new_image_listener(self, emitter): try: self.image_source = emitter method = OpencvMatchTemplate.matching_methods[self.matching_method] if type(self.matching_method) == str else self.matching_method res = cv2.matchTemplate(emitter.img, self.template_source.img, method) + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) top_left = max_loc w, h = self.template_source.img.shape[::-1] @@ -1086,11 +1097,12 @@ def on_new_image_listener(self, emitter): top_left = min_loc bottom_right = (top_left[0] + w, top_left[1] + h) - img = emitter.img.copy() - cv2.rectangle(img, top_left, bottom_right, 255, 2) - - self.set_image_data(img) - + if bool(self.show_result_rectangle): + img = emitter.img.copy() + cv2.rectangle(img, top_left, bottom_right, 255, 2) + self.set_image_data(img) + else: + self.set_image_data(res) self.on_matching_success(top_left[0], top_left[1], w, h) except Exception: print(traceback.format_exc()) From 0e72c7e6149a71d4b9cdd23a37524ed9e188c0b5 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 16 Feb 2021 12:19:47 +0100 Subject: [PATCH 043/110] Editor layout improved. --- editor/editor.py | 107 ++++++++++++++++++++------------------- editor/editor_widgets.py | 49 ++++++++++++------ 2 files changed, 88 insertions(+), 68 deletions(-) diff --git a/editor/editor.py b/editor/editor.py index cc4ecb93..cd00cde0 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -77,9 +77,9 @@ def setup(self, refWidget, newParent): def start_drag(self, emitter, x, y): self.active = True - self.app_instance.project.onmousemove.do(self.on_drag, js_prevent_default=True, js_stop_propagation=False) - self.app_instance.project.onmouseup.do(self.stop_drag, js_prevent_default=True, js_stop_propagation=False) - self.app_instance.project.onmouseleave.do(self.stop_drag, 0, 0, js_prevent_default=True, js_stop_propagation=False) + self.app_instance.mainContainer.onmousemove.do(self.on_drag, js_prevent_default=True, js_stop_propagation=False) + self.app_instance.mainContainer.onmouseup.do(self.stop_drag, js_prevent_default=True, js_stop_propagation=False) + self.app_instance.mainContainer.onmouseleave.do(self.stop_drag, 0, 0, js_prevent_default=True, js_stop_propagation=False) self.origin_x = -1 self.origin_y = -1 @@ -87,6 +87,9 @@ def start_drag(self, emitter, x, y): def stop_drag(self, emitter, x, y): self.active = False self.update_position() + self.app_instance.mainContainer.onmousemove.do(None, js_prevent_default=False, js_stop_propagation=True) + self.app_instance.mainContainer.onmouseup.do(None, js_prevent_default=False, js_stop_propagation=True) + self.app_instance.mainContainer.onmouseleave.do(None, 0, 0, js_prevent_default=False, js_stop_propagation=True) return () def on_drag(self, emitter, x, y): @@ -726,10 +729,31 @@ def main(self): """ self.page.children['head'].add_child('mycss', my_css_head) - self.mainContainer = gui.Container(width='100%', height='100%', layout_orientation=gui.Container.LAYOUT_VERTICAL, style={ - 'background-color': 'white', 'border': 'none', 'overflow': 'hidden'}) - - menubar = gui.MenuBar(height='4%') + self.mainContainer = gui.GridBox(width='100%', height='100%') + self.mainContainer.set_from_asciiart(""" + | menubar | menubar | instances | + | toolbox | toolbar | instances | + | toolbox | project | instances | + | toolbox | project | instances | + | toolbox | project | instances | + | toolbox | project | instances | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | project | properties | + | toolbox | signals | properties | + | toolbox | signals | properties | + | toolbox | signals | properties | + | toolbox | signals | properties | + | toolbox | signals | properties | + """, 0, 0) + + menubar = gui.MenuBar(width='100%', height='100%') menu = gui.Menu(width='100%', height='100%') menu.style['z-index'] = '1' m1 = gui.MenuItem('File', width=150, height='100%') @@ -759,7 +783,7 @@ def main(self): menubar.append(menu) self.toolbar = editor_widgets.ToolBar( - width='100%', height='30px', margin='0px 0px') + width='100%', height='100%', margin='0px 0px') self.toolbar.style['border-bottom'] = '1px solid rgba(0,0,0,.12)' self.toolbar.add_command( '/editor_resources:delete.png', self.toolbar_delete_clicked, 'Delete Widget') @@ -769,7 +793,7 @@ def main(self): '/editor_resources:paste.png', self.menu_paste_selection_clicked, 'Paste Widget') lbl = gui.Label("Snap grid", width=100) - self.spin_grid_size = gui.SpinBox('15', '1', '100', width=50) + self.spin_grid_size = gui.SpinBox('15', '1', '100', width=50, height="100%") self.spin_grid_size.onchange.do(self.on_snap_grid_size_change) grid_size = gui.HBox(children=[lbl, self.spin_grid_size], style={ @@ -797,52 +821,31 @@ def main(self): m3.onclick.do(self.menu_project_config_clicked) m4.onclick.do(self.menu_became_a_sponsor) - self.subContainer = gui.HBox( - width='100%', height='96%', layout_orientation=gui.Container.LAYOUT_HORIZONTAL) - self.subContainer.style.update({'position': 'relative', - 'overflow': 'hidden', - 'align-items': 'stretch'}) - # here are contained the widgets self.widgetsCollection = editor_widgets.WidgetCollection( - self, width='100%', height='50%') + self, width='100%', height='100%') self.projectConfiguration = editor_widgets.ProjectConfigurationDialog( 'Project Configuration', 'Write here the configuration for your project.') - self.attributeEditor = editor_widgets.EditorAttributes( - self, width='100%') - self.attributeEditor.style['overflow'] = 'hide' self.signalConnectionManager = editor_widgets.SignalConnectionManager( - width='100%', height='50%', style={'order': '1'}) + width='100%', height='100%', style={'order': '1'}) - self.mainContainer.append([menubar, self.subContainer]) + self.mainContainer.append( + {'toolbox': self.widgetsCollection, 'signals': self.signalConnectionManager, 'menubar':menubar}) - self.subContainerLeft = gui.VBox(width='20%', height='100%') - self.subContainerLeft.style['position'] = 'relative' - self.subContainerLeft.style['left'] = '0px' - self.widgetsCollection.style['order'] = '0' - self.subContainerLeft.append( - {'widgets_collection': self.widgetsCollection, 'signal_manager': self.signalConnectionManager}) - self.subContainerLeft.add_class('RaisedFrame') - - self.centralContainer = gui.VBox(width='56%', height='100%') - self.centralContainer.append(self.toolbar) - - self.subContainerRight = gui.Container(width='24%', height='100%') - self.subContainerRight.style.update( - {'position': 'absolute', 'right': '0px', 'overflow-y': 'auto', 'overflow-x': 'hidden'}) - self.subContainerRight.add_class('RaisedFrame') + self.mainContainer.append(self.toolbar,'toolbar') self.instancesWidget = editor_widgets.InstancesWidget(width='100%') self.instancesWidget.treeView.on_tree_item_selected.do( self.on_instances_widget_selection) - self.subContainerRight.append( - {'instances_widget': self.instancesWidget, 'attributes_editor': self.attributeEditor}) + self.attributeEditor = editor_widgets.EditorAttributes( + self, width='100%', height="100%") + self.attributeEditor.style['overflow'] = 'auto' - self.subContainer.append( - [self.subContainerLeft, self.centralContainer, self.subContainerRight]) + self.mainContainer.append( + {'instances': self.instancesWidget, 'properties': self.attributeEditor}) self.drag_helpers = [ResizeHelper(self, width=16, height=16), DragHelper(self, width=15, height=15), @@ -877,7 +880,7 @@ def on_snap_grid_size_change(self, emitter, value): def on_drag_resize_end(self, emitter): self.selectedWidget.__dict__[ - 'attributes_editor'].set_widget(self.selectedWidget) + 'properties'].set_widget(self.selectedWidget) def configure_widget_for_editing(self, widget): """ A widget have to be added to the editor, it is configured here in order to be conformant @@ -951,11 +954,11 @@ def on_widget_selection(self, widget): t = time.time() if issubclass(widget.__class__, gui.Container) or widget == None: - self.subContainerLeft.append( - self.widgetsCollection, 'widgets_collection') + self.mainContainer.append( + self.widgetsCollection, 'toolbox') else: - self.subContainerLeft.append(gui.Label("Cannot append widgets to %s class. It is not a container. Select a container" % - widget.__class__.__name__), 'widgets_collection') + self.mainContainer.append(gui.Label("Cannot append widgets to %s class. It is not a container. Select a container" % + widget.__class__.__name__), 'toolbox') if self.selectedWidget == widget or widget == self.project: self.selectedWidget = widget @@ -968,13 +971,13 @@ def on_widget_selection(self, widget): # self.selectedWidget.__dict__['signal_manager'] = editor_widgets.SignalConnectionManager(width='100%', height='50%', style={'order':'1'}) self.signalConnectionManager.update(self.selectedWidget, self.project) #self.subContainerLeft.append(self.selectedWidget.__dict__['signal_manager'], 'signal_manager') - if self.selectedWidget.__dict__.get('attributes_editor', None) is None: - self.selectedWidget.__dict__['attributes_editor'] = editor_widgets.EditorAttributes( - self, width='100%', style={'overflow': 'hide'}) + if self.selectedWidget.__dict__.get('properties', None) is None: + self.selectedWidget.__dict__['properties'] = editor_widgets.EditorAttributes( + self, width='100%', height='100%', style={'overflow': 'auto'}) self.selectedWidget.__dict__[ - 'attributes_editor'].set_widget(self.selectedWidget) - self.subContainerRight.append(self.selectedWidget.__dict__[ - 'attributes_editor'], 'attributes_editor') + 'properties'].set_widget(self.selectedWidget) + self.mainContainer.append(self.selectedWidget.__dict__[ + 'properties'], 'properties') parent = self.selectedWidget.get_parent() for drag_helper in self.drag_helpers: @@ -1005,7 +1008,7 @@ def menu_new_clicked(self, widget): return false;""" % {'evt': self.EVENT_ONDROPPPED} self.project.onkeydown.do(self.onkeydown) - self.centralContainer.append(self.project, 'project') + self.mainContainer.append(self.project, 'project') self.project.style['position'] = 'relative' self.tabindex = 0 # incremental number to allow widgets selection self.selectedWidget = None diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index 2296174c..59369b1e 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -65,17 +65,24 @@ def append_instances_from_tree(self, node, parent=None): class InstancesWidget(gui.VBox): def __init__(self, **kwargs): super(InstancesWidget, self).__init__(**kwargs) - self.titleLabel = gui.Label('Instances list', width='100%') + self.titleLabel = gui.Label('Instances list', width='100%', height=20) self.titleLabel.add_class("DialogTitle") self.style['align-items'] = 'flex-start' + + self.container = gui.VBox(width="100%", height="calc(100% - 20px)") + self.container.css_align_items = 'flex-start' + self.container.css_justify_content = 'flex-start' + self.container.css_overflow = 'auto' + self.treeView = InstancesTree() + self.container.append(self.treeView) - self.append([self.titleLabel, self.treeView]) + self.append([self.titleLabel, self.container]) self.titleLabel.style['order'] = '-1' self.titleLabel.style['-webkit-order'] = '-1' - self.treeView.style['order'] = '0' - self.treeView.style['-webkit-order'] = '0' + self.container.style['order'] = '0' + self.container.style['-webkit-order'] = '0' def update(self, editorProject, selectedNode): self.treeView.empty() @@ -88,10 +95,11 @@ def select(self, selectedNode): self.treeView.select_instance(self.treeView, selectedNode) -class ToolBar(gui.Container): +class ToolBar(gui.HBox): def __init__(self, **kwargs): super(ToolBar, self).__init__(**kwargs) - self.set_layout_orientation(gui.Container.LAYOUT_HORIZONTAL) + self.css_align_items = 'center' + self.css_justify_content = 'flex-start' self.style['background-color'] = 'white' def add_command(self, imagePath, callback, title): @@ -140,6 +148,8 @@ def __init__(self, widget, listenersList, eventConnectionFuncName, eventConnecti self.label.style.update({'float': 'left', 'font-size': '10px', 'overflow': 'hidden', 'outline': '1px solid lightgray'}) + self.label_do = gui.Label(".do ->", style={'white-space':'nowrap'}) + self.dropdownListeners = gui.DropDown(width='32%', height='100%') self.dropdownListeners.onchange.do(self.on_listener_selection) self.dropdownListeners.attributes['title'] = "The listener who will receive the event" @@ -188,7 +198,7 @@ def __init__(self, widget, listenersList, eventConnectionFuncName, eventConnecti print(dir(eventConnectionFunc.callback)) self.disconnect() - self.append([self.label, self.dropdownListeners, self.dropdownMethods]) + self.append([self.label, self.label_do, self.dropdownListeners, self.dropdownMethods]) def on_listener_selection(self, widget, dropDownValue): self.dropdownMethods.empty() @@ -563,9 +573,9 @@ class WidgetCollection(gui.Container): def __init__(self, appInstance, **kwargs): self.appInstance = appInstance super(WidgetCollection, self).__init__(**kwargs) - self.lblTitle = gui.Label("Widgets Toolbox") + self.lblTitle = gui.Label("Widgets Toolbox", height=20) self.lblTitle.add_class("DialogTitle") - self.widgetsContainer = gui.HBox(width='100%', height='85%') + self.widgetsContainer = gui.HBox(width='100%', height='calc(100% - 20px)') self.widgetsContainer.style.update({'overflow-y': 'scroll', 'overflow-x': 'hidden', 'align-items': 'flex-start', @@ -717,23 +727,30 @@ def __init__(self, appInstance, **kwargs): #self.style['overflow-y'] = 'scroll' self.style['justify-content'] = 'flex-start' self.style['-webkit-justify-content'] = 'flex-start' - self.titleLabel = gui.Label('Attributes editor', width='100%') + self.titleLabel = gui.Label('Attributes editor', width='100%', height=20) self.titleLabel.add_class("DialogTitle") - self.infoLabel = gui.Label('Selected widget: None') + self.infoLabel = gui.Label('Selected widget: None', height=25) self.infoLabel.style['font-weight'] = 'bold' - self.append([self.titleLabel, self.infoLabel]) + + self.attributes_groups_container = gui.VBox(width="100%", height="calc(100% - 45px)") + self.attributes_groups_container.css_overflow = 'auto' + self.attributes_groups_container.css_align_items = "flex-start" + self.attributes_groups_container.css_justify_content = "flex-start" + + self.append([self.titleLabel, self.infoLabel, self.attributes_groups_container]) self.titleLabel.style['order'] = '-1' self.titleLabel.style['-webkit-order'] = '-1' self.infoLabel.style['order'] = '0' self.infoLabel.style['-webkit-order'] = '0' + self.attributes_groups_container.style['order'] = '1' + self.attributes_groups_container.style['-webkit-order'] = '1' self.group_orders = { 'Generic': '2', 'WidgetSpecific': '3', 'Geometry': '4', 'Background': '5', 'Transformation': '6'} self.attributesInputs = list() # load editable attributes - self.append(self.titleLabel) self.attributeGroups = {} def update_widget(self): @@ -757,11 +774,11 @@ def set_widget(self, widget): self.infoLabel.set_text("Selected widget: %s" % widget.variable_name) for w in self.attributeGroups.values(): - self.remove_child(w) + self.attributes_groups_container.remove_child(w) for w in self.attributesInputs: if w.attributeDict['group'] in self.attributeGroups: - self.attributeGroups[w.attributeDict['group']].remove_child(w) + self.attributeGroups[w.attributeDict['group']].attributes_groups_container.remove_child(w) index = 100 default_width = "100%" @@ -841,7 +858,7 @@ def set_widget(self, widget): self.attributesInputs.append(attributeEditor) for w in self.attributeGroups.values(): - self.append(w) + self.attributes_groups_container.append(w) # widget that allows to edit a specific html and css attributes From 88b767730d54e21bf41fcbed25a7b1a35e6a93be Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 16 Feb 2021 12:52:22 +0100 Subject: [PATCH 044/110] Logo update. --- README.md | 2 +- remi/res/logo.png | Bin 11351 -> 16628 bytes remi/res/logo.svg | 276 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 268 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 76b7dbfa..b225651d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build Status](https://travis-ci.com/dddomodossola/remi.svg?branch=master)](https://travis-ci.com/dddomodossola/remi)

- +

diff --git a/remi/res/logo.png b/remi/res/logo.png index 0700cbd637b6e599910a6a97d88e6675e3897e24..0568befa301fd46aa9035dfcca648045f66b3b2b 100644 GIT binary patch literal 16628 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 Ll%*?Pn!Njek%?Nm diff --git a/remi/res/logo.svg b/remi/res/logo.svg index b12929f2..51a46aae 100644 --- a/remi/res/logo.svg +++ b/remi/res/logo.svg @@ -14,8 +14,8 @@ viewBox="0 0 210 297" version="1.1" id="svg8" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" - sodipodi:docname="remi_logo.svg"> + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="logo.svg"> + inkscape:window-maximized="1" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:snap-global="false"> @@ -2154,5 +2157,260 @@ style="fill:#ffffff;stroke-width:1.32291663" id="path191-5-4-4-1" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7b7b59fd4cb0eb987d2fce72347659b61fdb0ad9 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 22 Feb 2021 16:52:28 +0100 Subject: [PATCH 045/110] Fixed Editor load external widgets error. --- editor/widgets/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editor/widgets/__init__.py b/editor/widgets/__init__.py index 224abb62..f57657cf 100644 --- a/editor/widgets/__init__.py +++ b/editor/widgets/__init__.py @@ -21,7 +21,7 @@ def default_icon(name, view_w=2, view_h=0.6): try: from .toolbox_EPICS import EPICSBooleanButton, EPICSLed, EPICSValueMeterWidget, EPICSPlotPV, EPICSValueGaugeWidget -except ImportError: +except Exception: class EPICSPlaceholder(gui.Label): icon = default_icon("missing EPICS") def __init__(self, msg="In order to use EPICS widgets install pyepics \n" + traceback.format_exc()): @@ -32,7 +32,7 @@ def __init__(self, msg="In order to use EPICS widgets install pyepics \n" + trac from .toolbox_opencv import OpencvImRead, OpencvCrop, OpencvVideo, OpencvThreshold, OpencvSplit, OpencvCvtColor, OpencvAddWeighted, OpencvBitwiseNot, OpencvBitwiseAnd, OpencvBitwiseOr,\ OpencvBilateralFilter, OpencvBlurFilter, OpencvDilateFilter, OpencvErodeFilter, OpencvLaplacianFilter, OpencvCanny, OpencvFindContours, OpencvInRangeGrayscale,\ OpencvMatchTemplate -except ImportError: +except Exception: class OPENCVPlaceholder(gui.Label): icon = default_icon("missing OPENCV") def __init__(self, msg="In order to use OpenCv widgets install python-opencv \n" + traceback.format_exc()): @@ -43,7 +43,7 @@ def __init__(self, msg="In order to use OpenCv widgets install python-opencv \n" try: from .toolbox_siemens import PLCSiemens, SiemensButton, BitStatusWidget, WordEditWidget, ByteViewWidget -except ImportError: +except Exception: class SIEMENSPlaceholder(gui.Label): icon = default_icon("missing SIEMENS") def __init__(self, msg="In order to use Siemens widgets install python-snap7 \n" + traceback.format_exc()): From 9908c8e19d1dc479a86d9dbe6d7c4f921b8f6e80 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 24 Feb 2021 16:52:33 +0100 Subject: [PATCH 046/110] Moved additional widgets loading at javascript event, preventing the editor errors on load. --- editor/editor.py | 7 +++++++ editor/editor_widgets.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/editor/editor.py b/editor/editor.py index cd00cde0..fe20be5c 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -710,6 +710,7 @@ class Editor(App): EVENT_ONDROPPPED = "on_dropped" selectedWidget = None + additional_widgets_loaded = False def __init__(self, *args): editor_res_path = os.path.join(os.path.dirname(__file__), 'res') @@ -1182,6 +1183,12 @@ def show_error_dialog(self, title, message): error_dialog.cancel.style['display'] = 'none' error_dialog.show(self) + def onload(self, emitter): + if not self.additional_widgets_loaded: + print("loading additional widgets") + self.additional_widgets_loaded = True + self.widgetsCollection.load_additional_widgets() + def on_dropped(self, left, top): if len(left) < 1: diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index 59369b1e..633c7ff2 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -647,7 +647,7 @@ def __init__(self, appInstance, **kwargs): self.add_widget_to_collection(gui.SvgImage) self.add_widget_to_collection(gui.SvgGroup) - self.load_additional_widgets() + #self.load_additional_widgets() def load_additional_widgets(self): try: From 58ed1cb6372ee11b0ecc1bb1512ca2c056ab6522 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 26 Feb 2021 16:48:09 +0100 Subject: [PATCH 047/110] New release to pypi --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2d74e6ab..afec9eec 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,5 @@ except: del params['setup_requires'] params['use_scm_version'] = False - params['version'] = '2020.11.20' + params['version'] = '2021.02.26' setup(**params) From 44929520a3d2885fde0d4b0e40c86d600e94320b Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 2 Mar 2021 12:38:58 +0100 Subject: [PATCH 048/110] Editor bugfix. --- editor/editor.py | 2 +- editor/widgets/toolbox_opencv.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/editor.py b/editor/editor.py index fe20be5c..eb0ffa77 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -447,7 +447,7 @@ def repr_widget_for_editor(self, widget, first_node=False): if type(y) == property and not getattr(widget, x) is None: if hasattr(y.fget, "editor_attributes"): #if this property is visible for the editor _value = getattr(widget, x) - if type(_value) == str: + if type(_value) == type('') or type(_value) == type(u''): _value = '"%s"' % _value code_nested += prototypes.proto_property_setup % { 'varname': widgetVarName, 'property': x, 'value': _value} diff --git a/editor/widgets/toolbox_opencv.py b/editor/widgets/toolbox_opencv.py index 3cd374a6..79b67a14 100644 --- a/editor/widgets/toolbox_opencv.py +++ b/editor/widgets/toolbox_opencv.py @@ -90,6 +90,7 @@ def update(self, *args): if self.app_instance==None: self.app_instance = self.search_app_instance(self) if self.app_instance==None: + self.attributes['src'] = "/%s/get_image_data?index=00"%self.identifier #gui.load_resource(self.filename) return self.app_instance.execute_javascript(""" url = '/%(id)s/get_image_data?index=%(frame_index)s'; From 9d4ce8491de596055122961827df8b146ebfd2ef Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 2 Mar 2021 12:41:15 +0100 Subject: [PATCH 049/110] Update version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index afec9eec..abc87977 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,5 @@ except: del params['setup_requires'] params['use_scm_version'] = False - params['version'] = '2021.02.26' + params['version'] = '2021.03.02' setup(**params) From 12360f40ea4448dc4276e3cb80bbfb6972aa13a6 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Fri, 5 Mar 2021 16:00:15 +0100 Subject: [PATCH 050/110] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b225651d..7c8ed87d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Do you need support? There is also a **drag n drop GUI Editor**. Look at the [Editor](https://github.com/dddomodossola/remi/tree/master/editor) subfolder to download your copy. + +A demostrative video from the great REVVEN labs +

From e6ca627571f69bf58874f584776aac845bbd2f9c Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 27 Apr 2021 12:21:09 +0200 Subject: [PATCH 051/110] BugFix #439 . Widgets now should be displayed grayed when disabled. --- remi/res/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remi/res/style.css b/remi/res/style.css index 2eafb50f..be98fead 100644 --- a/remi/res/style.css +++ b/remi/res/style.css @@ -192,7 +192,7 @@ body.remi-main > div { background-color: white; z-index: 0; } -.remi-main[disabled]{ +.remi-main *[disabled='True']{ filter: grayscale(100%) opacity(0.6); pointer-events: none; } From 77c0f90beadd6415018b06a9e839e05c1e32fd68 Mon Sep 17 00:00:00 2001 From: xcodz-dot <71920621+xcodz-dot@users.noreply.github.com> Date: Thu, 27 May 2021 06:58:23 +0530 Subject: [PATCH 052/110] Update server.py --- remi/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remi/server.py b/remi/server.py index 0ff2de66..bf38eca4 100644 --- a/remi/server.py +++ b/remi/server.py @@ -842,7 +842,7 @@ def start(self): try: import android android.webbrowser.open(self._base_address) - except ImportError: + except (ImportError, AttributeError): # use default browser instead of always forcing IE on Windows if os.name == 'nt': webbrowser.get('windows-default').open(self._base_address) From 335068fd8a389c599fd494d17fa61eb9de79a516 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 21 Jun 2021 14:17:34 +0200 Subject: [PATCH 053/110] New AsciiContainer, an easy way to arrange layouts. --- remi/gui.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/remi/gui.py b/remi/gui.py index c33b5fd7..781ffdb9 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -1988,6 +1988,77 @@ def __init__(self, *args, **kwargs): self.css_flex_direction = 'column' +class AsciiContainer(Container): + widget_layout_map = None + + def __init__(self, *args, **kwargs): + Container.__init__(self, *args, **kwargs) + self.css_position = 'relative' + + def set_from_asciiart(self, asciipattern, gap_horizontal=0, gap_vertical=0): + """ + asciipattern (str): a multiline string representing the layout + | widget1 | + | widget1 | + | widget2 | widget3 | + gap_horizontal (int): a percent value + gap_vertical (int): a percent value + """ + pattern_rows = asciipattern.split('\n') + # remove empty rows + for r in pattern_rows[:]: + if len(r.replace(" ", "")) < 1: + pattern_rows.remove(r) + + layout_height_in_chars = len(pattern_rows) + self.widget_layout_map = {} + row_index = 0 + for row in pattern_rows: + row = row.strip() + row_width = len(row) - row.count('|') #the row width is calculated without pipes + row = row[1:-1] #removing |pipes at beginning and end + columns = row.split('|') + + left_value = 0 + for column in columns: + widget_key = column.strip() + widget_width = float(len(column)) + + if not widget_key in self.widget_layout_map.keys(): + #width is calculated in percent + # height is instead initialized at 1 and incremented by 1 each row the key is present + # at the end of algorithm the height will be converted in percent + self.widget_layout_map[widget_key] = { 'width': "%.2f%%"%float(widget_width / (row_width) * 100.0 - gap_horizontal), + 'height':1, + 'top':"%.2f%%"%float(row_index / (layout_height_in_chars) * 100.0 + (gap_vertical/2.0)), + 'left':"%.2f%%"%float(left_value / (row_width) * 100.0 + (gap_horizontal/2.0))} + else: + self.widget_layout_map[widget_key]['height'] += 1 + + left_value += widget_width + row_index += 1 + + #converting height values in percent string + for key in self.widget_layout_map.keys(): + self.widget_layout_map[key]['height'] = "%.2f%%"%float(self.widget_layout_map[key]['height'] / (layout_height_in_chars) * 100.0 - gap_vertical) + + for key in self.widget_layout_map.keys(): + self.set_widget_layout(key) + + def append(self, widget, key=''): + key = Container.append(self, widget, key) + self.set_widget_layout(key) + return key + + def set_widget_layout(self, widget_key): + if not ((widget_key in self.children.keys() and (widget_key in self.widget_layout_map.keys()))): + return + self.children[widget_key].css_position = 'absolute' + self.children[widget_key].set_size(self.widget_layout_map[widget_key]['width'], self.widget_layout_map[widget_key]['height']) + self.children[widget_key].css_left = self.widget_layout_map[widget_key]['left'] + self.children[widget_key].css_top = self.widget_layout_map[widget_key]['top'] + + class TabBox(Container): """ A multipage container. Add a tab by doing an append. ie. tabbox.append( widget, "Tab Name" ) From be2a1f73992f6b1f3c2bc39cef9238dc029228f9 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 3 Aug 2021 09:43:43 +0200 Subject: [PATCH 054/110] BugFix #457. --- remi/gui.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 781ffdb9..83a779cd 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -1376,14 +1376,7 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que // characters than they actually were Remi.prototype._byteLength = function(str) { // returns the byte length of an utf8 string - var s = str.length; - for (var i=str.length-1; i>=0; i--) { - var code = str.charCodeAt(i); - if (code > 0x7f && code <= 0x7ff) s++; - else if (code > 0x7ff && code <= 0xffff) s+=2; - if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate - } - return s; + return str.length; }; Remi.prototype._paramPacketize = function (ps){ From 7f8ff4812f3f813710eca6e70e7662ec9f1e0157 Mon Sep 17 00:00:00 2001 From: Davide Date: Mon, 1 Nov 2021 10:50:52 +0100 Subject: [PATCH 055/110] BugFix #469. Now MenuItems open with a click instead that mouse hover. --- remi/gui.py | 2 ++ remi/res/style.css | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 83a779cd..27bee3d4 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3986,6 +3986,8 @@ def __init__(self, text='', *args, **kwargs): self.type = 'li' self.set_text(text) + self.attributes['tabindex'] = '0' + def append(self, value, key=''): return self.sub_container.append(value, key=key) diff --git a/remi/res/style.css b/remi/res/style.css index be98fead..e0f95ef9 100644 --- a/remi/res/style.css +++ b/remi/res/style.css @@ -395,18 +395,18 @@ body.remi-main > div { .remi-main .MenuBar { border-bottom: 1px solid rgba(0, 0, 0, .26); } -.remi-main .Menu > .MenuItem:hover { +.remi-main .Menu > .MenuItem:focus-within { background: rgb(2, 70, 147); box-shadow: 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12), 0 2px 4px -1px rgba(0, 0, 0, .2); } -.remi-main .MenuItem .MenuItem:hover { +.remi-main .MenuItem .MenuItem:focus-within { background: rgba(4, 90, 188, 0.4); } .remi-main .Menu::after { content: ""; clear: both; } -.remi-main .MenuItem:hover > .Menu { +.remi-main .MenuItem:focus-within > .Menu { display: block; } .remi-main input.file { From 7806bf1ccd79f957af52dd6da17e332d3ae871ef Mon Sep 17 00:00:00 2001 From: Davide Date: Mon, 22 Nov 2021 23:11:35 +0100 Subject: [PATCH 056/110] BugFix #469 Now clicking on a leaf MenuItem makes the menu to be hidden. --- remi/gui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/remi/gui.py b/remi/gui.py index 27bee3d4..4683b8ef 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3992,6 +3992,12 @@ def append(self, value, key=''): return self.sub_container.append(value, key=key) + @decorate_set_on_listener("(self, emitter)") + @decorate_event_js("remi.sendCallback('%(emitter_identifier)s','%(event_name)s');document.activeElement.blur();") + def onclick(self): + """Called when the Widget gets clicked by the user with the left mouse button.""" + return () + class TreeView(Container): """TreeView widget can contain TreeItem.""" From 258d2a7e716060f662c8cc7f15045ec2c482bf13 Mon Sep 17 00:00:00 2001 From: Gilbert Brault Date: Fri, 26 Nov 2021 08:32:04 +0100 Subject: [PATCH 057/110] Remi behind jupyter server proxy (#475) * added proxy support * added proxy feature * extended gitignore * adapted editor to proxy * addes notebooks * patch videaoplayer for proxy * Delete JlabRemiHelloWorld-checkpoint.ipynb * Delete JlabRemiWidgets_Overview-checkpoint.ipynb * added notebooks * documenting nottebook usage * no .bak files * revert to original remi * remi proxy lean implementation * notebooks + small glitch with overload * leaner implementation * Update readme.md * Update readme.md * _callback glitch fixed in _process_all * cleaned _process_all unecessary code (overload) --- .gitignore | 6 + notebooks/JlabRemiEditor.ipynb | 227 +++++++++++ notebooks/JlabRemiHelloWorld.ipynb | 248 ++++++++++++ notebooks/JlabRemiWidgets_Overview.ipynb | 491 +++++++++++++++++++++++ notebooks/main.py | 4 + notebooks/readme.md | 74 ++++ remi/server.py | 54 ++- 7 files changed, 1086 insertions(+), 18 deletions(-) create mode 100644 notebooks/JlabRemiEditor.ipynb create mode 100644 notebooks/JlabRemiHelloWorld.ipynb create mode 100644 notebooks/JlabRemiWidgets_Overview.ipynb create mode 100644 notebooks/main.py create mode 100644 notebooks/readme.md diff --git a/.gitignore b/.gitignore index d22b3413..8ace3cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ __pycache__/ *.so # Distribution / packaging +package-lock.json +package.json +node_modules +.ipynb_checkpoints/ +venv/ .Python env/ build/ @@ -22,6 +27,7 @@ var/ *.egg-info/ .installed.cfg *.egg +.vscode # PyInstaller # Usually these files are written by a python script from a template diff --git a/notebooks/JlabRemiEditor.ipynb b/notebooks/JlabRemiEditor.ipynb new file mode 100644 index 00000000..0cc4ce64 --- /dev/null +++ b/notebooks/JlabRemiEditor.ipynb @@ -0,0 +1,227 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d6190bb3-da16-4cc8-b4ab-50ef32de2b96", + "metadata": {}, + "source": [ + "# Editor" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cd99ec06-e44c-4dc7-a0a0-f34caf7f2366", + "metadata": {}, + "outputs": [], + "source": [ + "import editor.prototypes\n", + "import editor.editor_widgets\n", + "from editor.editor import Editor\n", + "import remi.gui as gui\n", + "from remi import start, App\n", + "from threading import Timer, Thread\n", + "import re" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c838e89f-cd30-44d4-a984-b64f018c695d", + "metadata": {}, + "outputs": [], + "source": [ + "remiport = 8090" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f3928282-2156-4b4a-ba0b-76b72493f85a", + "metadata": {}, + "outputs": [], + "source": [ + "from threading import Timer, Thread" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a9a7d0f5-d32f-4555-9be0-f7077d446bc7", + "metadata": {}, + "outputs": [], + "source": [ + "class MyApp(Editor):\n", + " def __init__(self, *args):\n", + " super(MyApp, self).__init__(*args,)\n", + " \n", + " def _net_interface_ip(self):\n", + " ip = super()._net_interface_ip()\n", + " return ip + f\"/proxy/{remiport}\"\n", + " \n", + " def _overload(self, data, **kwargs):\n", + " if \"filename\" in kwargs:\n", + " filename = kwargs['filename']\n", + " else:\n", + " return data\n", + " paths = self.all_paths()\n", + " for pattern in paths.keys():\n", + " if ( filename.endswith(\".css\") or filename.endswith(\".html\") or filename.endswith(\".js\") or filename.endswith(\"internal\") ):\n", + " if type(data) == str:\n", + " data = re.sub(f\"/{pattern}:\", f\"/proxy/{remiport}/{pattern}:\", data)\n", + " else:\n", + " data = re.sub(f\"/{pattern}:\", f\"/proxy/{remiport}/{pattern}:\", data.decode()).encode()\n", + " return data\n", + "\n", + " def _process_all(self, func, **kwargs):\n", + " print(kwargs)\n", + " kwargs.update({\"overload\": self._overload})\n", + " super()._process_all(func, **kwargs)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "71c3725e-9235-4682-b304-a0e580ccc8df", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.server INFO Started httpserver http://127.0.0.1:8090/\n" + ] + } + ], + "source": [ + "myRemi = Thread(target=start, \n", + " args=(MyApp,),\n", + " kwargs={'address':'127.0.0.1', \n", + " 'port':remiport, \n", + " 'multiple_instance':True,\n", + " 'enable_file_cache':True, \n", + " 'update_interval':0.5, \n", + " 'start_browser':False})\n", + "myRemi.start() " + ] + }, + { + "cell_type": "markdown", + "id": "73cd1921-a5a1-424c-aa3b-3f3f2032cd06", + "metadata": {}, + "source": [ + "http://127.0.0.1:8888/proxy/8090" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6b211659-17bd-4fab-b945-fc56f2af4e06", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "616824c7-afdd-4e95-8c85-25d1be1d8c5e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.request INFO built UI (path=/)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "new project\n", + ">>>>>>>>>>>>startup time: 0.3019986152648926\n", + "{}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "127.0.0.1 - - [24/Nov/2021 18:37:45] \"GET / HTTP/1.1\" 200 -\n", + "remi.server.ws INFO connection established: ('127.0.0.1', 63958)\n", + "remi.server.ws INFO handshake complete\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "loading additional widgets\n", + "-------------used keys:[]\n", + "EditorAttributes set widget\n", + "selected widget: container0\n", + "selected widget class: Container\n", + "is widget Container: True\n", + "0.5186376571655273\n" + ] + } + ], + "source": [ + "IFrame(src=f\"http://localhost:8888/proxy/{remiport}/\",width=\"100%\",height=\"600px\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f87d351-bc19-4c5a-b6dd-ced963cd4efc", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/JlabRemiHelloWorld.ipynb b/notebooks/JlabRemiHelloWorld.ipynb new file mode 100644 index 00000000..1b233e7f --- /dev/null +++ b/notebooks/JlabRemiHelloWorld.ipynb @@ -0,0 +1,248 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "9c637d02-c7c6-4950-b2b5-7e8fe3148350", + "metadata": {}, + "outputs": [], + "source": [ + "import remi.gui as gui\n", + "from remi import start, App\n", + "from threading import Timer, Thread\n", + "import re" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c838e89f-cd30-44d4-a984-b64f018c695d", + "metadata": {}, + "outputs": [], + "source": [ + "remiport = 8085" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "96d858c6-acde-44f1-92f9-417f5617bc28", + "metadata": {}, + "outputs": [], + "source": [ + "class MyApp(App):\n", + " def __init__(self, *args):\n", + " super(MyApp, self).__init__(*args)\n", + " \n", + " def _net_interface_ip(self):\n", + " ip = super()._net_interface_ip()\n", + " return ip + f\"/proxy/{remiport}\"\n", + " \n", + " def _overload(self, data, **kwargs):\n", + " if \"filename\" in kwargs:\n", + " filename = kwargs['filename']\n", + " else:\n", + " return data\n", + " paths = self.all_paths()\n", + " for pattern in paths.keys():\n", + " if ( filename.endswith(\".css\") or filename.endswith(\".html\") or filename.endswith(\".js\") or filename.endswith(\"internal\") ):\n", + " if type(data) == str:\n", + " data = re.sub(f\"/{pattern}:\", f\"/proxy/{remiport}/{pattern}:\", data)\n", + " else:\n", + " data = re.sub(f\"/{pattern}:\", f\"/proxy/{remiport}/{pattern}:\", data.decode()).encode()\n", + " return data\n", + "\n", + " def _process_all(self, func, **kwargs):\n", + " print(kwargs)\n", + " kwargs.update({\"overload\": self._overload})\n", + " super()._process_all(func, **kwargs)\n", + "\n", + " def main(self):\n", + " #creating a container VBox type, vertical\n", + " wid = gui.VBox(width=300, height=200)\n", + "\n", + " #creating a text label\n", + " self.lbl = gui.Label('Hello', width='80%', height='50%')\n", + "\n", + " #a button for simple interaction\n", + " bt = gui.Button('Press me!', width=200, height=30)\n", + "\n", + " #setting up the listener for the click event\n", + " bt.onclick.connect(self.on_button_pressed)\n", + " \n", + " #adding the widgets to the main container\n", + " wid.append(self.lbl)\n", + " wid.append(bt)\n", + "\n", + " # returning the root widget\n", + " return wid\n", + "\n", + " # listener function\n", + " def on_button_pressed(self, emitter):\n", + " # print(\"button pressed\")\n", + " self.lbl.set_text('Hello World!')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a3ccabe9-a77e-45bc-bc2a-8632685a44be", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.server INFO Started httpserver http://127.0.0.1:8085/\n" + ] + } + ], + "source": [ + "myRemi = Thread(target=start, \n", + " args=(MyApp,),\n", + " kwargs={'address':'127.0.0.1', \n", + " 'port':remiport, \n", + " 'multiple_instance':True,\n", + " 'enable_file_cache':True, \n", + " 'update_interval':0.5, \n", + " 'start_browser':False})\n", + "myRemi.start() " + ] + }, + { + "cell_type": "markdown", + "id": "0dbd2b0e-85cc-481d-80e1-5df01037d302", + "metadata": {}, + "source": [ + "http://127.0.0.1:8888/proxy/8085/" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1c89e4b0-3a23-4484-8dc7-f3b538eb97f9", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "21abcb80-3780-4c36-b82d-c0f6b781d649", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.request INFO built UI (path=/)\n", + "127.0.0.1 - - [24/Nov/2021 17:05:29] \"GET / HTTP/1.1\" 200 -\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.server.ws INFO connection established: ('127.0.0.1', 49261)\n", + "remi.server.ws INFO handshake complete\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "127.0.0.1 - - [24/Nov/2021 17:09:26] \"GET / HTTP/1.1\" 200 -\n", + "remi.server.ws INFO connection established: ('127.0.0.1', 49323)\n", + "remi.server.ws INFO handshake complete\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "127.0.0.1 - - [24/Nov/2021 17:09:43] \"GET / HTTP/1.1\" 200 -\n", + "remi.server.ws INFO connection established: ('127.0.0.1', 49348)\n", + "remi.server.ws INFO handshake complete\n" + ] + } + ], + "source": [ + "IFrame(src=\"http://localhost:8888/proxy/8085/\",width=\"100%\",height=\"250px\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8d32fac-2a27-44a0-b3a3-8256e124c685", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/JlabRemiWidgets_Overview.ipynb b/notebooks/JlabRemiWidgets_Overview.ipynb new file mode 100644 index 00000000..42ed4d97 --- /dev/null +++ b/notebooks/JlabRemiWidgets_Overview.ipynb @@ -0,0 +1,491 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3c9af890-8b49-44fe-9279-4ef33aaaab2d", + "metadata": {}, + "outputs": [], + "source": [ + "import remi.gui as gui\n", + "from remi import start, App\n", + "from threading import Timer, Thread\n", + "import re" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d1eacbad-a65d-4a50-9340-27268629b703", + "metadata": {}, + "outputs": [], + "source": [ + "remiport = 8086" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "261c2b8e-0919-49be-9fd2-79264a69eac9", + "metadata": {}, + "outputs": [], + "source": [ + "class MyApp(App):\n", + " def __init__(self, *args):\n", + " super(MyApp, self).__init__(*args)\n", + "\n", + " def _net_interface_ip(self):\n", + " ip = super()._net_interface_ip()\n", + " return ip + f\"/proxy/{remiport}\"\n", + " \n", + " def _overload(self, data, **kwargs):\n", + " if \"filename\" in kwargs:\n", + " filename = kwargs['filename']\n", + " else:\n", + " return data\n", + " paths = self.all_paths()\n", + " for pattern in paths.keys():\n", + " if ( filename.endswith(\".css\") or filename.endswith(\".html\") or filename.endswith(\".js\") or filename.endswith(\"internal\") ):\n", + " if type(data) == str:\n", + " data = re.sub(f\"/{pattern}:\", f\"/proxy/{remiport}/{pattern}:\", data)\n", + " else:\n", + " data = re.sub(f\"/{pattern}:\", f\"/proxy/{remiport}/{pattern}:\", data.decode()).encode()\n", + " return data\n", + "\n", + " def _process_all(self, func, **kwargs):\n", + " print(kwargs)\n", + " kwargs.update({\"overload\": self._overload})\n", + " super()._process_all(func, **kwargs)\n", + "\n", + " def idle(self):\n", + " self.counter.set_text('Running Time: ' + str(self.count))\n", + " self.progress.set_value(self.count%100)\n", + "\n", + " def main(self):\n", + " # the margin 0px auto centers the main container\n", + " verticalContainer = gui.Container(width=540, margin='0px auto', style={'display': 'block', 'overflow': 'hidden'})\n", + "\n", + " horizontalContainer = gui.Container(width='100%', layout_orientation=gui.Container.LAYOUT_HORIZONTAL, margin='0px', style={'display': 'block', 'overflow': 'auto'})\n", + " \n", + " subContainerLeft = gui.Container(width=320, style={'display': 'block', 'overflow': 'auto', 'text-align': 'center'})\n", + " self.img = gui.Image(f'/res:logo.png', height=100, margin='10px')\n", + " self.img.onclick.do(self.on_img_clicked)\n", + "\n", + " self.table = gui.Table.new_from_list([('ID', 'First Name', 'Last Name'),\n", + " ('101', 'Danny', 'Young'),\n", + " ('102', 'Christine', 'Holand'),\n", + " ('103', 'Lars', 'Gordon'),\n", + " ('104', 'Roberto', 'Robitaille'),\n", + " ('105', 'Maria', 'Papadopoulos')], width=300, height=200, margin='10px')\n", + " self.table.on_table_row_click.do(self.on_table_row_click)\n", + "\n", + " # the arguments are\twidth - height - layoutOrientationOrizontal\n", + " subContainerRight = gui.Container(style={'width': '220px', 'display': 'block', 'overflow': 'auto', 'text-align': 'center'})\n", + " self.count = 0\n", + " self.counter = gui.Label('', width=200, height=30, margin='10px')\n", + "\n", + " self.lbl = gui.Label('This is a LABEL!', width=200, height=30, margin='10px')\n", + "\n", + " self.bt = gui.Button('Press me!', width=200, height=30, margin='10px')\n", + " # setting the listener for the onclick event of the button\n", + " self.bt.onclick.do(self.on_button_pressed)\n", + "\n", + " self.txt = gui.TextInput(width=200, height=30, margin='10px')\n", + " self.txt.set_text('This is a TEXTAREA')\n", + " self.txt.onchange.do(self.on_text_area_change)\n", + "\n", + " self.spin = gui.SpinBox(1, 0, 100, width=200, height=30, margin='10px')\n", + " self.spin.onchange.do(self.on_spin_change)\n", + "\n", + " self.progress = gui.Progress(1, 100, width=200, height=5)\n", + "\n", + " self.check = gui.CheckBoxLabel('Label checkbox', True, width=200, height=30, margin='10px')\n", + " self.check.onchange.do(self.on_check_change)\n", + "\n", + " self.btInputDiag = gui.Button('Open InputDialog', width=200, height=30, margin='10px')\n", + " self.btInputDiag.onclick.do(self.open_input_dialog)\n", + "\n", + " self.btFileDiag = gui.Button('File Selection Dialog', width=200, height=30, margin='10px')\n", + " self.btFileDiag.onclick.do(self.open_fileselection_dialog)\n", + "\n", + " self.btUploadFile = gui.FileUploader('./', width=200, height=30, margin='10px')\n", + " self.btUploadFile.onsuccess.do(self.fileupload_on_success)\n", + " self.btUploadFile.onfailed.do(self.fileupload_on_failed)\n", + "\n", + " items = ('Danny Young','Christine Holand','Lars Gordon','Roberto Robitaille')\n", + " self.listView = gui.ListView.new_from_list(items, width=300, height=120, margin='10px')\n", + " self.listView.onselection.do(self.list_view_on_selected)\n", + "\n", + " self.link = gui.Link(\"http://localhost:8081\", \"A link to here\", width=200, height=30, margin='10px')\n", + "\n", + " self.dropDown = gui.DropDown.new_from_list(('DropDownItem 0', 'DropDownItem 1'),\n", + " width=200, height=20, margin='10px')\n", + " self.dropDown.onchange.do(self.drop_down_changed)\n", + " self.dropDown.select_by_value('DropDownItem 0')\n", + "\n", + " self.slider = gui.Slider(10, 0, 100, 5, width=200, height=20, margin='10px')\n", + " self.slider.onchange.do(self.slider_changed)\n", + "\n", + " self.colorPicker = gui.ColorPicker('#ffbb00', width=200, height=20, margin='10px')\n", + " self.colorPicker.onchange.do(self.color_picker_changed)\n", + "\n", + " self.date = gui.Date('2015-04-13', width=200, height=20, margin='10px')\n", + " self.date.onchange.do(self.date_changed)\n", + "\n", + " self.video = gui.Widget( _type='iframe', width=290, height=200, margin='10px')\n", + " self.video.attributes['src'] = \"https://drive.google.com/file/d/0B0J9Lq_MRyn4UFRsblR3UTBZRHc/preview\"\n", + " self.video.attributes['width'] = '100%'\n", + " self.video.attributes['height'] = '100%'\n", + " self.video.attributes['controls'] = 'true'\n", + " self.video.style['border'] = 'none'\n", + " \n", + " self.tree = gui.TreeView(width='100%', height=300)\n", + " ti1 = gui.TreeItem(\"Item1\")\n", + " ti2 = gui.TreeItem(\"Item2\")\n", + " ti3 = gui.TreeItem(\"Item3\")\n", + " subti1 = gui.TreeItem(\"Sub Item1\")\n", + " subti2 = gui.TreeItem(\"Sub Item2\")\n", + " subti3 = gui.TreeItem(\"Sub Item3\")\n", + " subti4 = gui.TreeItem(\"Sub Item4\")\n", + " subsubti1 = gui.TreeItem(\"Sub Sub Item1\")\n", + " subsubti2 = gui.TreeItem(\"Sub Sub Item2\")\n", + " subsubti3 = gui.TreeItem(\"Sub Sub Item3\")\n", + " self.tree.append([ti1, ti2, ti3])\n", + " ti2.append([subti1, subti2, subti3, subti4])\n", + " subti4.append([subsubti1, subsubti2, subsubti3])\n", + " \n", + " # appending a widget to another, the first argument is a string key\n", + " subContainerRight.append([self.counter, self.lbl, self.bt, self.txt, self.spin, self.progress, self.check, self.btInputDiag, self.btFileDiag])\n", + " # use a defined key as we replace this widget later\n", + " fdownloader = gui.FileDownloader('download test', '../remi/res/logo.png', width=200, height=30, margin='10px')\n", + " subContainerRight.append(fdownloader, key='file_downloader')\n", + " subContainerRight.append([self.btUploadFile, self.dropDown, self.slider, self.colorPicker, self.date, self.tree])\n", + " self.subContainerRight = subContainerRight\n", + "\n", + " subContainerLeft.append([self.img, self.table, self.listView, self.link, self.video])\n", + "\n", + " horizontalContainer.append([subContainerLeft, subContainerRight])\n", + "\n", + " menu = gui.Menu(width='100%', height='30px')\n", + " m1 = gui.MenuItem('File', width=100, height=30)\n", + " m2 = gui.MenuItem('View', width=100, height=30)\n", + " m2.onclick.do(self.menu_view_clicked)\n", + " m11 = gui.MenuItem('Save', width=100, height=30)\n", + " m12 = gui.MenuItem('Open', width=100, height=30)\n", + " m12.onclick.do(self.menu_open_clicked)\n", + " m111 = gui.MenuItem('Save', width=100, height=30)\n", + " m111.onclick.do(self.menu_save_clicked)\n", + " m112 = gui.MenuItem('Save as', width=100, height=30)\n", + " m112.onclick.do(self.menu_saveas_clicked)\n", + " m3 = gui.MenuItem('Dialog', width=100, height=30)\n", + " m3.onclick.do(self.menu_dialog_clicked)\n", + "\n", + " menu.append([m1, m2, m3])\n", + " m1.append([m11, m12])\n", + " m11.append([m111, m112])\n", + "\n", + " menubar = gui.MenuBar(width='100%', height='30px')\n", + " menubar.append(menu)\n", + "\n", + " verticalContainer.append([menubar, horizontalContainer])\n", + "\n", + " #this flag will be used to stop the display_counter Timer\n", + " self.stop_flag = False \n", + "\n", + " # kick of regular display of counter\n", + " self.display_counter()\n", + "\n", + " # returning the root widget\n", + " return verticalContainer\n", + "\n", + " def display_counter(self):\n", + " self.count += 1\n", + " if not self.stop_flag:\n", + " Timer(1, self.display_counter).start()\n", + "\n", + " def menu_dialog_clicked(self, widget):\n", + " self.dialog = gui.GenericDialog(title='Dialog Box', message='Click Ok to transfer content to main page', width='500px')\n", + " self.dtextinput = gui.TextInput(width=200, height=30)\n", + " self.dtextinput.set_value('Initial Text')\n", + " self.dialog.add_field_with_label('dtextinput', 'Text Input', self.dtextinput)\n", + "\n", + " self.dcheck = gui.CheckBox(False, width=200, height=30)\n", + " self.dialog.add_field_with_label('dcheck', 'Label Checkbox', self.dcheck)\n", + " values = ('Danny Young', 'Christine Holand', 'Lars Gordon', 'Roberto Robitaille')\n", + " self.dlistView = gui.ListView.new_from_list(values, width=200, height=120)\n", + " self.dialog.add_field_with_label('dlistView', 'Listview', self.dlistView)\n", + "\n", + " self.ddropdown = gui.DropDown.new_from_list(('DropDownItem 0', 'DropDownItem 1'),\n", + " width=200, height=20)\n", + " self.dialog.add_field_with_label('ddropdown', 'Dropdown', self.ddropdown)\n", + "\n", + " self.dspinbox = gui.SpinBox(min=0, max=5000, width=200, height=20)\n", + " self.dspinbox.set_value(50)\n", + " self.dialog.add_field_with_label('dspinbox', 'Spinbox', self.dspinbox)\n", + "\n", + " self.dslider = gui.Slider(10, 0, 100, 5, width=200, height=20)\n", + " self.dspinbox.set_value(50)\n", + " self.dialog.add_field_with_label('dslider', 'Slider', self.dslider)\n", + "\n", + " self.dcolor = gui.ColorPicker(width=200, height=20)\n", + " self.dcolor.set_value('#ffff00')\n", + " self.dialog.add_field_with_label('dcolor', 'Colour Picker', self.dcolor)\n", + "\n", + " self.ddate = gui.Date(width=200, height=20)\n", + " self.ddate.set_value('2000-01-01')\n", + " self.dialog.add_field_with_label('ddate', 'Date', self.ddate)\n", + "\n", + " self.dialog.confirm_dialog.do(self.dialog_confirm)\n", + " self.dialog.show(self)\n", + "\n", + " def dialog_confirm(self, widget):\n", + " result = self.dialog.get_field('dtextinput').get_value()\n", + " self.txt.set_value(result)\n", + "\n", + " result = self.dialog.get_field('dcheck').get_value()\n", + " self.check.set_value(result)\n", + "\n", + " result = self.dialog.get_field('ddropdown').get_value()\n", + " self.dropDown.select_by_value(result)\n", + "\n", + " result = self.dialog.get_field('dspinbox').get_value()\n", + " self.spin.set_value(result)\n", + "\n", + " result = self.dialog.get_field('dslider').get_value()\n", + " self.slider.set_value(result)\n", + "\n", + " result = self.dialog.get_field('dcolor').get_value()\n", + " self.colorPicker.set_value(result)\n", + "\n", + " result = self.dialog.get_field('ddate').get_value()\n", + " self.date.set_value(result)\n", + "\n", + " result = self.dialog.get_field('dlistView').get_value()\n", + " self.listView.select_by_value(result)\n", + "\n", + " # listener function\n", + " def on_img_clicked(self, widget):\n", + " self.lbl.set_text('Image clicked!')\n", + "\n", + " def on_table_row_click(self, table, row, item):\n", + " self.lbl.set_text('Table Item clicked: ' + item.get_text())\n", + "\n", + " def on_button_pressed(self, widget):\n", + " self.lbl.set_text('Button pressed! ')\n", + " self.bt.set_text('Hi!')\n", + "\n", + " def on_text_area_change(self, widget, newValue):\n", + " self.lbl.set_text('Text Area value changed!')\n", + "\n", + " def on_spin_change(self, widget, newValue):\n", + " self.lbl.set_text('SpinBox changed, new value: ' + str(newValue))\n", + "\n", + " def on_check_change(self, widget, newValue):\n", + " self.lbl.set_text('CheckBox changed, new value: ' + str(newValue))\n", + "\n", + " def open_input_dialog(self, widget):\n", + " self.inputDialog = gui.InputDialog('Input Dialog', 'Your name?',\n", + " initial_value='type here', \n", + " width=500)\n", + " self.inputDialog.confirm_value.do(\n", + " self.on_input_dialog_confirm)\n", + "\n", + " # here is returned the Input Dialog widget, and it will be shown\n", + " self.inputDialog.show(self)\n", + "\n", + " def on_input_dialog_confirm(self, widget, value):\n", + " self.lbl.set_text('Hello ' + value)\n", + "\n", + " def open_fileselection_dialog(self, widget):\n", + " self.fileselectionDialog = gui.FileSelectionDialog('File Selection Dialog', 'Select files and folders', False,\n", + " '.')\n", + " self.fileselectionDialog.confirm_value.do(\n", + " self.on_fileselection_dialog_confirm)\n", + "\n", + " # here is returned the Input Dialog widget, and it will be shown\n", + " self.fileselectionDialog.show(self)\n", + "\n", + " def on_fileselection_dialog_confirm(self, widget, filelist):\n", + " # a list() of filenames and folders is returned\n", + " self.lbl.set_text('Selected files: %s' % ','.join(filelist))\n", + " if len(filelist):\n", + " f = filelist[0]\n", + " # replace the last download link\n", + " fdownloader = gui.FileDownloader(\"download selected\", f, width=200, height=30)\n", + " self.subContainerRight.append(fdownloader, key='file_downloader')\n", + "\n", + " def list_view_on_selected(self, widget, selected_item_key):\n", + " \"\"\" The selection event of the listView, returns a key of the clicked event.\n", + " You can retrieve the item rapidly\n", + " \"\"\"\n", + " self.lbl.set_text('List selection: ' + self.listView.children[selected_item_key].get_text())\n", + "\n", + " def drop_down_changed(self, widget, value):\n", + " self.lbl.set_text('New Combo value: ' + value)\n", + "\n", + " def slider_changed(self, widget, value):\n", + " self.lbl.set_text('New slider value: ' + str(value))\n", + "\n", + " def color_picker_changed(self, widget, value):\n", + " self.lbl.set_text('New color value: ' + value)\n", + "\n", + " def date_changed(self, widget, value):\n", + " self.lbl.set_text('New date value: ' + value)\n", + "\n", + " def menu_save_clicked(self, widget):\n", + " self.lbl.set_text('Menu clicked: Save')\n", + "\n", + " def menu_saveas_clicked(self, widget):\n", + " self.lbl.set_text('Menu clicked: Save As')\n", + "\n", + " def menu_open_clicked(self, widget):\n", + " self.lbl.set_text('Menu clicked: Open')\n", + "\n", + " def menu_view_clicked(self, widget):\n", + " self.lbl.set_text('Menu clicked: View')\n", + "\n", + " def fileupload_on_success(self, widget, filename):\n", + " self.lbl.set_text('File upload success: ' + filename)\n", + "\n", + " def fileupload_on_failed(self, widget, filename):\n", + " self.lbl.set_text('File upload failed: ' + filename)\n", + "\n", + " def on_close(self):\n", + " \"\"\" Overloading App.on_close event to stop the Timer.\n", + " \"\"\"\n", + " self.stop_flag = True\n", + " super(MyApp, self).on_close()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "288af5fb-7a9c-4d0b-b7bc-6acb3146eb7d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.server INFO Started httpserver http://127.0.0.1:8086/\n" + ] + } + ], + "source": [ + "myRemi = Thread(target=start, \n", + " args=(MyApp,),\n", + " kwargs={'address':'127.0.0.1', \n", + " 'port':remiport, \n", + " 'multiple_instance':True,\n", + " 'enable_file_cache':True, \n", + " 'update_interval':0.5, \n", + " 'start_browser':False,\n", + " })\n", + "myRemi.start() " + ] + }, + { + "cell_type": "markdown", + "id": "e7548e87-1260-4190-9947-1b5677554998", + "metadata": {}, + "source": [ + "http://127.0.0.1:8888/proxy/8086/" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c5435343-168e-4a00-a4d2-e245c622335b", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "edfabb0f-36bc-4114-bdc5-6bbf25dc8d17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.request INFO built UI (path=/)\n", + "127.0.0.1 - - [24/Nov/2021 17:20:26] \"GET / HTTP/1.1\" 200 -\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "remi.server.ws INFO connection established: ('127.0.0.1', 49592)\n", + "remi.server.ws INFO handshake complete\n" + ] + } + ], + "source": [ + "IFrame(src=f\"http://localhost:8888/proxy/{remiport}/\",width=\"100%\",height=\"600px\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1ae2367-e0be-4332-a2aa-542afcabc8a2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/main.py b/notebooks/main.py new file mode 100644 index 00000000..c921dc7d --- /dev/null +++ b/notebooks/main.py @@ -0,0 +1,4 @@ +from jupyterlab.labapp import main +import sys + +sys.exit(main()) \ No newline at end of file diff --git a/notebooks/readme.md b/notebooks/readme.md new file mode 100644 index 00000000..d2eee1b1 --- /dev/null +++ b/notebooks/readme.md @@ -0,0 +1,74 @@ +# Notebooks with remi + +## Install + +In a virtual environment + +`pip install jupyterlab, jupyter-server-proxy` + +jupyter-server-proxy is the proxy we use for jupyter lab + +Pythonnet needs Visual Studio Build Tools 2019 and needs github install in Windows + +`pip install git+https://github.com/pythonnet/pythonnet.git` + +pip install pywebview + +## Notebooks + +* JlabRemiHelloWorld.ipynb => the "HelloWorld" application +* JlabRemiWidgets_Overview.ipynb => the remi widget overview app +* JlabRemiEditor.ipynb => the remi 'IDE' + +## Using... + +### Create a Proxy class + +* 8085 is supposed to be the remi port on localhost + +```python +remiport = 8085 +# overload _net_interface_ip, _overload and _process_all +class MyApp(Editor): + def __init__(self, *args): + super(MyApp, self).__init__(*args,) + + def _net_interface_ip(self): + # used for the ws(s) address + ip = super()._net_interface_ip() + return ip + f"/proxy/{remiport}" + + def _overload(self, data, **kwargs): + # every content sent back to a client needs /res: to be overloaded and replaced by /proxy/8085/res: (holds for editor_resources and the like) + if "filename" in kwargs: + filename = kwargs['filename'] + else: + return data + paths = self.all_paths() + for pattern in paths.keys(): + if ( filename.endswith(".css") or filename.endswith(".html") or filename.endswith(".js") or filename.endswith("internal") ): + if type(data) == str: + data = re.sub(f"/{pattern}:", f"/proxy/{remiport}/{pattern}:", data) + else: + data = re.sub(f"/{pattern}:", f"/proxy/{remiport}/{pattern}:", data.decode()).encode() + return data + +``` + +* start as usual + +```python +myRemi = Thread(target=start, + args=(MyApp,), + kwargs={'address':'127.0.0.1', + 'port':{remiport}, + 'multiple_instance':True, + 'enable_file_cache':True, + 'update_interval':0.5, + 'start_browser':False}) +``` +If no overload provided, remi behaves as legacy + +## Disclaimer + +Only jupyter-server-proxy was tested, but other proxy should work diff --git a/remi/server.py b/remi/server.py index bf38eca4..39353d51 100644 --- a/remi/server.py +++ b/remi/server.py @@ -128,6 +128,7 @@ def __init__(self, headers, request, client_address, server, *args, **kwargs): self.server = server self.handshake_done = False self._log = logging.getLogger('remi.server.ws') + #self._log.setLevel(logging.DEBUG) socketserver.StreamRequestHandler.__init__(self, request, client_address, server, *args, **kwargs) def setup(self): @@ -163,6 +164,10 @@ def read_next_message(self): except ValueError: # socket was closed, just return without errors return False + if length is None: + return False + if len(length) < 2: + return False length = self.bytetonum(length[1]) & 127 if length == 126: length = struct.unpack('>H', self.rfile.read(2))[0] @@ -172,6 +177,7 @@ def read_next_message(self): decoded = '' for char in self.rfile.read(length): decoded += chr(self.bytetonum(char) ^ masks[len(decoded) % 4]) + self._log.debug('read_message: %s...' % (decoded[:10])) self.on_message(from_websocket(decoded)) except socket.timeout: return False @@ -184,6 +190,9 @@ def send_message(self, message): if not self.handshake_done: self._log.warning("ignoring message %s (handshake not done)" % message[:10]) return False + + if message[0] == "2": + i = 0 self._log.debug('send_message: %s... -> %s' % (message[:10], self.client_address)) out = bytearray() @@ -411,11 +420,13 @@ def _instance(self): if hasattr(client, '_update_thread'): self._update_thread = client._update_thread - net_interface_ip = self.headers.get('Host', "%s:%s"%(self.connection.getsockname()[0],self.server.server_address[1])) + net_interface_ip = self._net_interface_ip() websocket_timeout_timer_ms = str(self.server.websocket_timeout_timer_ms) pending_messages_queue_length = str(self.server.pending_messages_queue_length) self.page.children['head'].set_internal_js(str(id(self)), net_interface_ip, pending_messages_queue_length, websocket_timeout_timer_ms) + def _net_interface_ip(self): + return self.headers.get('Host', "%s:%s"%(self.connection.getsockname()[0],self.server.server_address[1])) def main(self, *_): """ Subclasses of App class *must* declare a main function that will be the entry point of the application. @@ -470,13 +481,13 @@ def do_gui_update(self): for widget in changed_widget_dict.keys(): html = changed_widget_dict[widget] __id = str(widget.identifier) - self._send_spontaneous_websocket_message(_MSG_UPDATE + __id + ',' + to_websocket(html)) + self._send_spontaneous_websocket_message(_MSG_UPDATE + __id + ',' + to_websocket(self._overload(html, filename="internal"))) self._need_update_flag = False def websocket_handshake_done(self, ws_instance_to_update): msg = "" with self.update_lock: - msg = "0" + self.root.identifier + ',' + to_websocket(self.page.children['body'].innerHTML({})) + msg = "0" + self.root.identifier + ',' + to_websocket(self._overload(self.page.children['body'].innerHTML({}), filename="internal")) ws_instance_to_update.send_message(msg) def set_root_widget(self, widget): @@ -487,7 +498,7 @@ def set_root_widget(self, widget): self.root._parent = self self.root.enable_refresh() - msg = "0" + self.root.identifier + ',' + to_websocket(self.page.children['body'].innerHTML({})) + msg = "0" + self.root.identifier + ',' + to_websocket(self._overload(self.page.children['body'].innerHTML({}), filename="internal")) self._send_spontaneous_websocket_message(msg) def _send_spontaneous_websocket_message(self, message): @@ -495,7 +506,7 @@ def _send_spontaneous_websocket_message(self, message): # noinspection PyBroadException try: if ws.send_message(message): - #if message sent ok, continue with nect client + #if message sent ok, continue with next client continue except Exception: self._log.error("sending websocket spontaneous message", exc_info=True) @@ -623,6 +634,15 @@ def do_GET(self): except Exception: self._log.error('error processing GET request', exc_info=True) + def all_paths(self): + paths = {'res': os.path.join(os.path.dirname(__file__), "res")} + static_paths = self._app_args.get('static_file_path', {}) + if not type(static_paths)==dict: + self._log.error("App's parameter static_file_path must be a Dictionary.", exc_info=False) + static_paths = {} + paths.update(static_paths) + return paths + def _get_static_file(self, filename): filename = filename.replace("..", "") #avoid backdirs __i = filename.find(':') @@ -631,19 +651,17 @@ def _get_static_file(self, filename): key = filename[:__i] path = filename[__i+1:] key = key.replace("/","") - paths = {'res': os.path.join(os.path.dirname(__file__), "res")} - static_paths = self._app_args.get('static_file_path', {}) - if not type(static_paths)==dict: - self._log.error("App's parameter static_file_path must be a Dictionary.", exc_info=False) - static_paths = {} - paths.update(static_paths) + paths = self.all_paths() if not key in paths: return None return os.path.join(paths[key], path) + + def _overload(self, data, **kwargs): + """Used to overload the content before sent back to client""" + return data - def _process_all(self, func): + def _process_all(self, func, **kwargs): self._log.debug('get: %s' % func) - static_file = self.re_static_file.match(func) attr_call = self.re_attr_call.match(func) @@ -658,7 +676,7 @@ def _process_all(self, func): page_content = self.page.repr() self.wfile.write(encode_text("\n")) - self.wfile.write(encode_text(page_content)) + self.wfile.write(encode_text(self._overload(page_content, filename="internal"))) elif static_file: filename = self._get_static_file(static_file.groups()[0]) @@ -673,7 +691,7 @@ def _process_all(self, func): self.end_headers() with open(filename, 'rb') as f: content = f.read() - self.wfile.write(content) + self.wfile.write(self._overload(content, filename=filename)) elif attr_call: with self.update_lock: param_dict = parse_qs(urlparse(func).query) @@ -701,9 +719,9 @@ def _process_all(self, func): self.send_header(k, headers[k]) self.end_headers() try: - self.wfile.write(content) + self.wfile.write(self._overload(content, filename="internal")) except TypeError: - self.wfile.write(encode_text(content)) + self.wfile.write(self._overload(encode_text(content), filename="internal")) def close(self): """ Command to initiate an App to close @@ -842,7 +860,7 @@ def start(self): try: import android android.webbrowser.open(self._base_address) - except (ImportError, AttributeError): + except ImportError: # use default browser instead of always forcing IE on Windows if os.name == 'nt': webbrowser.get('windows-default').open(self._base_address) From 528ee6ac64c2e384181e861bac1bd6c0e28ca6f9 Mon Sep 17 00:00:00 2001 From: Davide Date: Mon, 20 Dec 2021 00:28:15 +0100 Subject: [PATCH 058/110] Fixing travis test problem. --- examples/grid_layout_app.py | 1 - remi/gui.py | 2 +- test/test_examples_app.py | 4 ++-- test/test_widget.py | 8 ++++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/grid_layout_app.py b/examples/grid_layout_app.py index 362e2bd4..11140a96 100644 --- a/examples/grid_layout_app.py +++ b/examples/grid_layout_app.py @@ -43,7 +43,6 @@ def main(self): text = gui.TextInput() - main_container.set_from_asciiart(""" |label |button | |label |text | diff --git a/remi/gui.py b/remi/gui.py index 4683b8ef..fdaaac17 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -4798,7 +4798,7 @@ def add_coord(self, x, y): class SvgPolygon(SvgPolyline, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): - def __init__(self, _maxlen=None, *args, **kwargs): + def __init__(self, _maxlen=1000, *args, **kwargs): super(SvgPolygon, self).__init__(_maxlen, *args, **kwargs) self.type = 'polygon' diff --git a/test/test_examples_app.py b/test/test_examples_app.py index 083cec7c..669dd4e8 100644 --- a/test/test_examples_app.py +++ b/test/test_examples_app.py @@ -232,8 +232,8 @@ def test_main(self): class TestPageInternalsApp(unittest.TestCase): @classmethod def setUpClass(cls): - import page_internals_app - cls.AppClass = page_internals_app.MyApp + import template_advanced_app + cls.AppClass = template_advanced_app.MyApp def setUp(self): self.AppClass.log_request = (lambda x,y:None) diff --git a/test/test_widget.py b/test/test_widget.py index 00d32a56..cf69b994 100755 --- a/test/test_widget.py +++ b/test/test_widget.py @@ -62,7 +62,7 @@ class TestTabBox(unittest.TestCase): def test_init(self): w = gui.TabBox() l = gui.Label('testTabBox_label') - w.add_tab(l, name='testtabbox', tab_cb=None) + w.add_tab(l, key='testtabbox', callback=None) self.assertIn('testTabBox_label',w.repr()) assertValidHTML(w.repr()) @@ -323,14 +323,14 @@ def test_init(self): widget = gui.Svg(width=10, height=10) assertValidHTML(widget.repr()) -class TestSvgShape(unittest.TestCase): +class TestSvgSubcontainer(unittest.TestCase): def test_init(self): - widget = gui.SvgShape(10, 10) + widget = gui.SvgSubcontainer(0, 0, 100, 100) assertValidHTML(widget.repr()) class TestSvgGroup(unittest.TestCase): def test_init(self): - widget = gui.SvgGroup(10, 10) + widget = gui.SvgGroup() assertValidHTML(widget.repr()) class TestSvgRectangle(unittest.TestCase): From ebf2352a1eec3b186a80c08fe0586cf4dbd8b211 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 20 Dec 2021 14:24:02 +0100 Subject: [PATCH 059/110] BugFix AsciiContainer - unashtable dict_keys. --- remi/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 27bee3d4..ac863ee2 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -2017,7 +2017,7 @@ def set_from_asciiart(self, asciipattern, gap_horizontal=0, gap_vertical=0): widget_key = column.strip() widget_width = float(len(column)) - if not widget_key in self.widget_layout_map.keys(): + if not widget_key in list(self.widget_layout_map.keys()): #width is calculated in percent # height is instead initialized at 1 and incremented by 1 each row the key is present # at the end of algorithm the height will be converted in percent @@ -2044,7 +2044,7 @@ def append(self, widget, key=''): return key def set_widget_layout(self, widget_key): - if not ((widget_key in self.children.keys() and (widget_key in self.widget_layout_map.keys()))): + if not ((widget_key in list(self.children.keys()) and (widget_key in list(self.widget_layout_map.keys())))): return self.children[widget_key].css_position = 'absolute' self.children[widget_key].set_size(self.widget_layout_map[widget_key]['width'], self.widget_layout_map[widget_key]['height']) From 7c2526f2ab9a367bbbf1cd8fd2d404783a42d2df Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 30 Dec 2021 23:24:08 +0100 Subject: [PATCH 060/110] Sponsors mentioned in readme. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 7c8ed87d..4a5ed2b4 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,18 @@ Remi is a GUI library for Python applications that gets rendered in web browsers This allows you to access your interface locally and remotely.

+ +

+Proud to be sponsored by + + + + + + +

+ + Do you need support?

Reddit - (subreddit RemiGUI) From 2c208054cc57ae610277084e41ed8e183b8fd727 Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 30 Dec 2021 23:31:18 +0100 Subject: [PATCH 061/110] Sponsors mentioned in readme. --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4a5ed2b4..975cf2a4 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,21 @@ This allows you to access your interface locally and remotely.

-

Proud to be sponsored by - +=== +

+ - +

+

+ -

- +

Do you need support? +===

Reddit - (subreddit RemiGUI)

From 0d4668204cf7f2ac68f497df4777a759c3888c28 Mon Sep 17 00:00:00 2001 From: Fabrice Fontaine Date: Mon, 7 Feb 2022 08:16:11 +0100 Subject: [PATCH 062/110] MANIFEST.in: add LICENSE file (#477) Add LICENSE file to MANIFEST.in so LICENSE will be added to the release tarball Signed-off-by: Fabrice Fontaine --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 3049b911..aec9cb12 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include remi/res/* include editor/* -include editor/res/* \ No newline at end of file +include editor/res/* +include LICENSE From 2293513d70be318afe0bcc8dd7214902908539a0 Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 15 Feb 2022 01:43:42 +1100 Subject: [PATCH 063/110] Add an example of a text to speech app using javascript execution and callbacks (#479) --- examples/text_to_speech_app.py | 108 +++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 examples/text_to_speech_app.py diff --git a/examples/text_to_speech_app.py b/examples/text_to_speech_app.py new file mode 100644 index 00000000..1f3270d6 --- /dev/null +++ b/examples/text_to_speech_app.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +import remi.gui as gui +from remi import start, App + +""" +This demo allows to test the Text To Speech (TTS) capabilities +of your browser via Remi. +Author: Sammy Pfeiffer +""" + +class MyApp(App): + def __init__(self, *args): + super(MyApp, self).__init__(*args) + + def main(self): + # We need to get dynamically the available voices + self.voices_dict = {} + # Default English_(Great_Britain) voice + self.selected_voice_id = 78 + self.voices_dropdown = gui.DropDown.new_from_list(["Loading voices list..."], + width=200, height=20, margin='10px') + self.container = gui.VBox(width=400) + self.lbl = gui.Label("Text to say:") + self.text_input = gui.TextInput(width=300) + self.lbl_rate = gui.Label("Rate (speed) to say:") + self.rate_slider = gui.Slider(1.0, min=0.1, max=5.0, step=0.1) + self.lbl_pitch = gui.Label("Pitch of voice:") + self.pitch_slider = gui.Slider(1.0, min=0.1, max=2.0, step=0.1) + self.bt_say = gui.Button("Say") + self.bt_say.onclick.do(self.on_say) + + self.container.append(self.lbl) + self.container.append(self.text_input) + self.container.append(self.lbl_rate) + self.container.append(self.rate_slider) + self.container.append(self.lbl_pitch) + self.container.append(self.pitch_slider) + self.container.append(self.bt_say) + self.container.append(self.voices_dropdown, key=99999) + + # returning the root widget + return self.container + + def idle(self): + """ + Using the idle function so we can get the available voices when the app opens + """ + if not self.voices_dict: + self._get_available_voices() + + def _voices_callback(self, **kwargs): + # print("_voices_callback args: {}".format(kwargs)) + self.voices_dict = kwargs + voice_options = kwargs.keys() + # Show the voice options sorted alphabetically + voice_options = sorted(voice_options) + # Sometimes we get an empty list, then do nothing, we will try later again + if voice_options: + self.container.remove_child(self.voices_dropdown) + self.voices_dropdown = gui.DropDown.new_from_list(voice_options, + width=200, height=20, margin='10px') + self.voices_dropdown.onchange.do(self._drop_down_changed) + self.container.append(self.voices_dropdown) + self.voices_dropdown.select_by_value("English_(Great_Britain)") + + def _drop_down_changed(self, widget, value): + self.selected_voice_id = int(self.voices_dict[value]) + print("Chosen: {} {}".format(value, self.selected_voice_id)) + + + # listener function + def _get_available_voices(self): + # Here we get the voices and store the one we want + self.execute_javascript(""" + var synth = window.speechSynthesis; + voices = synth.getVoices(); + console.log(voices); + var return_params = {}; + for(voice_id in voices){ + console.log(voices[voice_id].name); + // Some voices have non-ascii characters and it makes remi crash + // as the characters have a size bigger than one character + // and the internal parsing code can't deal with it currently + name_ascii = voices[voice_id].name.replace(/[\u{0080}-\u{FFFF}]/gu,""); + return_params[name_ascii] = String(voice_id); + } + remi.sendCallbackParam('%(id)s','%(callback_function)s', return_params)""" % { + 'id': str(id(self)), + 'callback_function': '_voices_callback'}) + + def on_say(self, widget): + text = self.text_input.get_text() + pitch = self.pitch_slider.get_value() + rate = self.rate_slider.get_value() + print("Saying: {} at rate {} and pitch {}".format(text, rate, pitch)) + self.execute_javascript( + """ + var synth = window.speechSynthesis; + voices = synth.getVoices(); + var utterThis = new SpeechSynthesisUtterance("{}"); + utterThis.pitch = {}; + utterThis.rate = {}; + utterThis.voice = voices[{}]; + synth.speak(utterThis); + """.format(text, pitch, rate, self.selected_voice_id)) + +# starts the web server +start(MyApp, address="0.0.0.0", port=9990) From 20c4c994b1ff648b1ba3fa5ba0513959b6a9e6e4 Mon Sep 17 00:00:00 2001 From: ulda Date: Mon, 14 Feb 2022 17:30:47 +0100 Subject: [PATCH 064/110] fix TabBox title size calculation (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove debug print geändert: remi/gui.py * fix rounding problems on TabBox tabsize calculation geändert: remi/gui.py Co-authored-by: Ulf Dambacher --- remi/gui.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index b3306246..23a70aeb 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -2066,9 +2066,15 @@ def __init__(self, *args, **kwargs): self.tab_keys_ordered_list = [] def resize_tab_titles(self): - tab_w = 100.0 / len(self.container_tab_titles.children.values()) + nch=len(self.container_tab_titles.children.values()) + # the rounding errors of "%.1f" add upt to more than 100.0% (e.g. at 7 tabs) , so be more precise here + tab_w = 1000.0 // nch /10 for l in self.container_tab_titles.children.values(): l.set_size("%.1f%%" % tab_w, "auto") + # and make last tab consume the rounding rest, looks better + last_tab_w=100.0-tab_w*(nch-1) + l.set_size("%.1f%%" % last_tab_w, "auto") + def append(self, widget, key=''): """ Adds a new tab. @@ -2101,7 +2107,7 @@ def remove_child(self, widget): @decorate_set_on_listener("(self, emitter, key)") @decorate_event def on_tab_selection(self, emitter, key): - print(str(key)) + #print(str(key)) for k in self.children.keys(): w = self.children[k] if w is self.container_tab_titles: From 64a053f558bf95ba67d0eff735ebef7bb7b8adc5 Mon Sep 17 00:00:00 2001 From: Davide Date: Tue, 22 Feb 2022 00:17:26 +0100 Subject: [PATCH 065/110] BugFix #480. Now WebSocket fin bit is used to parse a message only when downloaded completely. --- remi/server.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/remi/server.py b/remi/server.py index 39353d51..23f9b838 100644 --- a/remi/server.py +++ b/remi/server.py @@ -159,25 +159,31 @@ def bytetonum(b): def read_next_message(self): # noinspection PyBroadException try: - try: - length = self.rfile.read(2) - except ValueError: - # socket was closed, just return without errors - return False - if length is None: - return False - if len(length) < 2: - return False - length = self.bytetonum(length[1]) & 127 - if length == 126: - length = struct.unpack('>H', self.rfile.read(2))[0] - elif length == 127: - length = struct.unpack('>Q', self.rfile.read(8))[0] - masks = [self.bytetonum(byte) for byte in self.rfile.read(4)] decoded = '' - for char in self.rfile.read(length): - decoded += chr(self.bytetonum(char) ^ masks[len(decoded) % 4]) - self._log.debug('read_message: %s...' % (decoded[:10])) + fin = 0 + while fin == 0: + head = None + try: + head = self.rfile.read(2) + except ValueError: + # socket was closed, just return without errors + return False + if head is None: + return False + if len(head) < 2: + return False + opcode = head[0] & 0b1111 + fin = head[0] >> 7 & 1 + length = self.bytetonum(head[1]) & 127 + if length == 126: + length = struct.unpack('>H', self.rfile.read(2))[0] + elif length == 127: + length = struct.unpack('>Q', self.rfile.read(8))[0] + masks = [self.bytetonum(byte) for byte in self.rfile.read(4)] + + for char in self.rfile.read(length): + decoded += chr(self.bytetonum(char) ^ masks[len(decoded) % 4]) + self._log.debug('read_message: %s...' % (decoded[:10])) self.on_message(from_websocket(decoded)) except socket.timeout: return False From 28eea6b7cd5365dabb31addac6386e6f827825ba Mon Sep 17 00:00:00 2001 From: Russ Frank Date: Sat, 26 Feb 2022 12:20:51 -0800 Subject: [PATCH 066/110] fix unmasking for continuation frames (#483) --- remi/server.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/remi/server.py b/remi/server.py index 23f9b838..17d8cb71 100644 --- a/remi/server.py +++ b/remi/server.py @@ -174,15 +174,27 @@ def read_next_message(self): return False opcode = head[0] & 0b1111 fin = head[0] >> 7 & 1 + is_masked = head[1] >> 7 & 1 length = self.bytetonum(head[1]) & 127 + if length == 126: length = struct.unpack('>H', self.rfile.read(2))[0] elif length == 127: length = struct.unpack('>Q', self.rfile.read(8))[0] - masks = [self.bytetonum(byte) for byte in self.rfile.read(4)] - + + masks = [] + if is_masked: + masks = [self.bytetonum(byte) for byte in self.rfile.read(4)] + + frame_data = '' for char in self.rfile.read(length): - decoded += chr(self.bytetonum(char) ^ masks[len(decoded) % 4]) + if is_masked: + next_data = chr(self.bytetonum(char) ^ masks[len(frame_data) % 4]) + frame_data += next_data + else: + frame_data += chr(self.bytetonum(char)) + + decoded += frame_data self._log.debug('read_message: %s...' % (decoded[:10])) self.on_message(from_websocket(decoded)) except socket.timeout: From 2c89af316b31d372307c1d3b4ffe9d9052631971 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 7 Mar 2022 07:20:23 +0100 Subject: [PATCH 067/110] Version release 2022.03.07. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index abc87977..401105da 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,5 @@ except: del params['setup_requires'] params['use_scm_version'] = False - params['version'] = '2021.03.02' + params['version'] = '2022.03.07' setup(**params) From 9eeb9c2be52a7ed460c4bf02c4a7ec255b7d7a58 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 9 Mar 2022 10:04:08 +0100 Subject: [PATCH 068/110] Stunning new Editor functionality. Auto-reload project on external file change. --- editor/editor.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/editor/editor.py b/editor/editor.py index eb0ffa77..f29fb865 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -27,6 +27,7 @@ import threading import traceback +import time if remi.server.pyLessThan3: import imp @@ -319,6 +320,7 @@ class Project(gui.Container): This class loads and save the project file, and also compiles a project in python code. """ + lastUpdateTime = 0 def __init__(self, **kwargs): super(Project, self).__init__(**kwargs) @@ -329,7 +331,14 @@ def __init__(self, **kwargs): 'background-image': "url('/editor_resources:background.png')"}) self.attr_editor_newclass = True + def shouldUpdate(self, filePathName): + #returns True if project file changed externally + if os.stat(filePathName).st_mtime > self.lastUpdateTime: + return True + return False + def load(self, ifile, configuration): + self.lastUpdateTime = os.stat(ifile).st_mtime self.ifile = ifile _module = load_source(self.ifile) @@ -665,6 +674,7 @@ def prepare_path_to_this_widget(self, node): self.prepare_path_to_this_widget(child) def save(self, save_path_filename, configuration): + compiled_code = '' code_classes = '' @@ -705,6 +715,8 @@ def save(self, save_path_filename, configuration): f.write(compiled_code) f.close() + self.lastUpdateTime = os.stat(save_path_filename).st_mtime + class Editor(App): EVENT_ONDROPPPED = "on_dropped" @@ -712,6 +724,8 @@ class Editor(App): selectedWidget = None additional_widgets_loaded = False + projectPathFilename = None + def __init__(self, *args): editor_res_path = os.path.join(os.path.dirname(__file__), 'res') super(Editor, self).__init__( @@ -721,6 +735,11 @@ def idle(self): for drag_helper in self.drag_helpers: drag_helper.update_position() + if self.projectPathFilename != None and len(self.projectPathFilename) > 0: + if self.project.shouldUpdate(self.projectPathFilename): + print("Project changed externally - RELOADING PROJECT") + self.reload_project() + def main(self): import time t= time.time() @@ -1019,6 +1038,17 @@ def menu_new_clicked(self, widget): if 'root' in self.project.children.keys(): self.project.remove_child(self.project.children['root']) + def reload_project(self): + self.menu_new_clicked(None) + try: + widgetTree = self.project.load( + self.projectPathFilename, self.projectConfiguration) + if widgetTree != None: + self.add_widget_to_editor(widgetTree) + except Exception: + self.show_error_dialog("ERROR: Unable to load the project", + "There were an error during project load: %s" % traceback.format_exc()) + def on_open_dialog_confirm(self, widget, filelist): if len(filelist): self.menu_new_clicked(None) From 02a6f2902646e402bbec14f9e41e60b148d11b64 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 14 Mar 2022 10:39:17 +0100 Subject: [PATCH 069/110] Changed username from dddomodossola to rawpython. --- README.md | 26 +++++++------------------- doc/contributors.md | 35 ----------------------------------- examples/minefield_app.py | 2 +- 3 files changed, 8 insertions(+), 55 deletions(-) delete mode 100644 doc/contributors.md diff --git a/README.md b/README.md index 975cf2a4..cc870174 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build Status](https://travis-ci.com/dddomodossola/remi.svg?branch=master)](https://travis-ci.com/dddomodossola/remi)

- +

@@ -34,7 +34,7 @@ Do you need support?

-There is also a **drag n drop GUI Editor**. Look at the [Editor](https://github.com/dddomodossola/remi/tree/master/editor) subfolder to download your copy. +There is also a **drag n drop GUI Editor**. Look at the [Editor](https://github.com/rawpython/remi/tree/master/editor) subfolder to download your copy. A demostrative video from the great REVVEN labs @@ -48,7 +48,7 @@ For a **stable** version: pip install remi ``` -For the most updated **experimental** version [Download](https://github.com/dddomodossola/remi/archive/master.zip) or check out Remi from git and install +For the most updated **experimental** version [Download](https://github.com/rawpython/remi/archive/master.zip) or check out Remi from git and install ``` python setup.py install @@ -56,10 +56,10 @@ python setup.py install or install directly using pip ``` -pip install git+https://github.com/dddomodossola/remi.git +pip install git+https://github.com/rawpython/remi.git ``` -Then start the test script (download it from github https://github.com/dddomodossola/remi/blob/master/examples/widgets_overview_app.py): +Then start the test script (download it from github https://github.com/rawpython/remi/blob/master/examples/widgets_overview_app.py): ``` python widgets_overview_app.py ``` @@ -70,7 +70,7 @@ Remi Platform independent Python GUI library. In less than 100 Kbytes of source code, perfect for your diet.

- +

Remi enables developers to create platform independent GUI with Python. The entire GUI is rendered in your browser. **No HTML** is required, Remi automatically translates your Python code into HTML. When your app starts, it starts a web server that will be accessible on your network. @@ -324,7 +324,7 @@ I suggest using the browser as a standard interface window. However, you can avoid using the browser. This can be simply obtained joining REMI and [PyWebView](https://github.com/r0x0r/pywebview). -Here is an example about this [standalone_app.py](https://github.com/dddomodossola/remi/blob/development/examples/standalone_app.py). +Here is an example about this [standalone_app.py](https://github.com/rawpython/remi/blob/development/examples/standalone_app.py). **Be aware that PyWebView uses qt, gtk and so on to create the window. An outdated version of these libraries can cause UI problems. If you experience UI issues, update these libraries, or better avoid standalone execution.** @@ -366,18 +366,6 @@ The library itself doesn't implement security strategies, and so it is advised t When loading data from external sources, consider protecting the application from potential javascript injection before displaying the content directly. -Contributors -=== -Thank you for collaborating with us to make Remi better! - -The real power of opensource is contributors. Please feel free to participate in this project, and consider to add yourself to the [contributors list](doc/contributors.md). -Yes, I know that GitHub already provides a list of contributors, but I feel that I must mention who helps. - - - - - - Projects using Remi === [PySimpleGUI](https://github.com/PySimpleGUI/PySimpleGUI): Launched in 2018 Actively developed and supported. Supports tkinter, Qt, WxPython, Remi (in browser). Create custom layout GUI's simply. Python 2.7 & 3 Support. 100+ Demo programs & Cookbook for rapid start. Extensive documentation. diff --git a/doc/contributors.md b/doc/contributors.md deleted file mode 100644 index e27a56ad..00000000 --- a/doc/contributors.md +++ /dev/null @@ -1,35 +0,0 @@ -[Davide Rosa](https://github.com/dddomodossola) - -[John Stowers](https://github.com/nzjrs) - -[Claudio Cannatà](https://github.com/cyberpro4) - -[Sam Pfeiffer](https://github.com/awesomebytes) - -[Ken Thompson](https://github.com/KenT2) - -[Paarth Tandon](https://github.com/Paarthri) - -[Ally Weir](https://github.com/allyjweir) - -[Timothy Cyrus](https://github.com/tcyrus) - -[John Hunter Bowen](https://github.com/jhb188) - -[Martin Spasov](https://github.com/SuburbanFilth) - -[Wellington Castello](https://github.com/wcastello) - -[PURPORC](https://github.com/PURPORC) - -[ttufts](https://github.com/ttufts) - -[Chris Braun](https://github.com/cryzed) - -[Alan Yorinks](https://github.com/MrYsLab) - -[Bernhard E. Reiter](https://github.com/bernhardreiter) - -[saewoonam](https://github.com/saewoonam) - -[Kavindu Santhusa](https://github.com/Ksengine) diff --git a/examples/minefield_app.py b/examples/minefield_app.py index f531f286..81146aaf 100644 --- a/examples/minefield_app.py +++ b/examples/minefield_app.py @@ -145,7 +145,7 @@ def main(self): self.minecount = 0 # mine number in the map self.flagcount = 0 # flag placed by the players - self.link = gui.Link("https://github.com/dddomodossola/remi", + self.link = gui.Link("https://github.com/rawpython/remi", "This is an example of REMI gui library.") self.link.set_size(1000, 20) self.link.style['margin'] = '10px' From 1a70d7ba7d806af8dd611ce9d31996de11d55d51 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 14 Mar 2022 10:41:17 +0100 Subject: [PATCH 070/110] Changed username from dddomodossola to rawpython. --- editor/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/editor/README.md b/editor/README.md index 789d9391..1237d4e2 100644 --- a/editor/README.md +++ b/editor/README.md @@ -9,9 +9,9 @@ What is Remi? [![Join the chat at https://gitter.im/dddomodossola/remi](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dddomodossola/remi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) RemI is a easy to use GUI library for Python. It shows in web browser and is accessible remotely. This removes platform-specific dependencies and lets you easily develop cross-platform applications in Python! -[More info at https://github.com/dddomodossola/remi](https://github.com/dddomodossola/remi) +[More info at https://github.com/rawpython/remi](https://github.com/rawpython/remi) -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/preview.png "Editor window") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/preview.png "Editor window") The **editor_app** allows you to graphically design your gui interface in an easy to use environment. From a collection of widgets (on the left side of the screen) you can choose the right one you would like to add to your interface. @@ -41,60 +41,60 @@ Now, let's create our first *Hello World* application. First of all we have to select from the left side toolbox the Widget component. It will be our main window. In the shown dialog we have to write a name for the variable. We will call it *mainContainer*. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/new_container.png "New Widget container") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/new_container.png "New Widget container") Then, once the widget is added to the editor, you can drag and resize it. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/drag_resize_container.png "Drag and resize container") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/drag_resize_container.png "Drag and resize container") Now, from the left side toolbox we select a Label widget that will contain our *Hello World* message. Again, we have to type the variable name for this widget. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/new_label.png "Add new label") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/new_label.png "Add new label") Then, we can select the label by clicking on it in order to drag and resize. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/drag_resize_label.png "Drag and resize label") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/drag_resize_label.png "Drag and resize label") We need for sure a Button. Since we have to add it to the mainContainer, we have to select the container by clicking on it. After that, click on the Button widget in the left side toolbox. Type the variable name and confirm. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/new_button.png "Add new button") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/new_button.png "Add new button") Select the Button widget by clicking on it and drag and resize. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/drag_resize_button.png "Drag and resize button") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/drag_resize_button.png "Drag and resize button") Now, all the required widgets are added. We have to connect the *onclick* event from the button to a listener, in our case the listener will be the main App. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/connect_button.png "Connect button onclick event to App") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/connect_button.png "Connect button onclick event to App") All it's done, save the project by the upper menu bar. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/save_menu.png "Save menu") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/save_menu.png "Save menu") Select the destination folder. Type the app filename and confirm. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/save_dialog.png "Save dialog") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/save_dialog.png "Save dialog") We can now edit the code to say the *Hello World* message. -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/edit_hello_message.png "Edit the code to say Hello World") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/edit_hello_message.png "Edit the code to say Hello World") Run the application and... Say Hello! -![Alt text](https://raw.githubusercontent.com/dddomodossola/remi/master/editor/res/tutorial_images/hello.png "Run the App") +![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/tutorial_images/hello.png "Run the App") Project configuration From f2dcb2c43ad764231fd487563acdc4bcb71f513b Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 14 Mar 2022 10:42:19 +0100 Subject: [PATCH 071/110] Changed username from dddomodossola to rawpython. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 401105da..6d6bd20e 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,8 @@ 'use_scm_version':{'version_scheme': 'post-release'}, 'long_description':long_description, 'long_description_content_type':"text/markdown", - 'url':"https://github.com/dddomodossola/remi", - 'download_url':"https://github.com/dddomodossola/remi/archive/master.zip", + 'url':"https://github.com/rawpython/remi", + 'download_url':"https://github.com/rawpython/remi/archive/master.zip", 'keywords':["gui-library", "remi", "platform-independent", "ui", "gui"], 'author':"Davide Rosa", 'author_email':"dddomodossola@gmail.com", From 0860dc71a2ef8f967a50a284233a03aba216c825 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 3 Apr 2022 15:50:04 +0200 Subject: [PATCH 072/110] Editor video tutorial. --- editor/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/editor/README.md b/editor/README.md index 1237d4e2..6439e4e6 100644 --- a/editor/README.md +++ b/editor/README.md @@ -11,6 +11,20 @@ What is Remi? RemI is a easy to use GUI library for Python. It shows in web browser and is accessible remotely. This removes platform-specific dependencies and lets you easily develop cross-platform applications in Python! [More info at https://github.com/rawpython/remi](https://github.com/rawpython/remi) + +**Editor Tutorial Videos** + + + + + +
+ 0 - Simple Hello World
+ Here is shown how to create a simple Hello World application. +
+ +
+ ![Alt text](https://raw.githubusercontent.com/rawpython/remi/master/editor/res/preview.png "Editor window") The **editor_app** allows you to graphically design your gui interface in an easy to use environment. From c2db5b9727a7598399004c5d3944a848a5fbc27e Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 3 Apr 2022 16:55:20 +0200 Subject: [PATCH 073/110] Editor video tutorial. --- editor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/README.md b/editor/README.md index 6439e4e6..82ae40cb 100644 --- a/editor/README.md +++ b/editor/README.md @@ -20,7 +20,7 @@ RemI is a easy to use GUI library for Python. It shows in web browser and is acc Here is shown how to create a simple Hello World application. - + https://youtu.be/2gWkRuj_CyQ From 01dd24dc1a55157958ea1d1f02cf7dc6d3e583e4 Mon Sep 17 00:00:00 2001 From: nick-hebi <50836262+nick-hebi@users.noreply.github.com> Date: Thu, 12 May 2022 15:55:36 -0400 Subject: [PATCH 074/110] Fix query string REGEX (#490) This change would allow for query string values to accept non-alphanumeric characters to conform with https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 --- remi/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remi/server.py b/remi/server.py index 17d8cb71..cd451489 100644 --- a/remi/server.py +++ b/remi/server.py @@ -342,7 +342,7 @@ class App(BaseHTTPRequestHandler, object): """ re_static_file = re.compile(r"^([\/]*[\w\d]+:[-_. $@?#£'%=()\/\[\]!+°§^,\w\d]+)") #https://regex101.com/r/uK1sX1/6 - re_attr_call = re.compile(r"^/*(\w+)\/(\w+)\?{0,1}(\w*\={1}(\w|\.)+\&{0,1})*$") + re_attr_call = re.compile(r"^/*(\w+)\/(\w+)\?{0,1}(\w*\={1}([^&])+\&{0,1})*$") #https://regex101.com/r/UTJB6N/1 def __init__(self, request, client_address, server, **app_args): self._app_args = app_args From 7df85f5b0b483deb9eda6ef337499eb13ed485ec Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 7 Jul 2022 14:53:33 +0200 Subject: [PATCH 075/110] Fixed strange behaviour in Editor during widgets dragging/resizing. --- editor/editor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editor/editor.py b/editor/editor.py index f29fb865..ddb9b8fb 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -88,9 +88,9 @@ def start_drag(self, emitter, x, y): def stop_drag(self, emitter, x, y): self.active = False self.update_position() - self.app_instance.mainContainer.onmousemove.do(None, js_prevent_default=False, js_stop_propagation=True) - self.app_instance.mainContainer.onmouseup.do(None, js_prevent_default=False, js_stop_propagation=True) - self.app_instance.mainContainer.onmouseleave.do(None, 0, 0, js_prevent_default=False, js_stop_propagation=True) + self.app_instance.mainContainer.onmousemove.do(None, js_prevent_default=True, js_stop_propagation=True) + self.app_instance.mainContainer.onmouseup.do(None, js_prevent_default=True, js_stop_propagation=True) + self.app_instance.mainContainer.onmouseleave.do(None, 0, 0, js_prevent_default=True, js_stop_propagation=True) return () def on_drag(self, emitter, x, y): From 498c6890b0bbb841387cb2d4166c82e366d488bc Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 27 Jul 2022 09:58:22 +0200 Subject: [PATCH 076/110] BugFix TabBox.remove_child. Division by 0 is now prevented. --- remi/gui.py | 11 ++++++++--- setup.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 23a70aeb..f363b849 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -2068,12 +2068,17 @@ def __init__(self, *args, **kwargs): def resize_tab_titles(self): nch=len(self.container_tab_titles.children.values()) # the rounding errors of "%.1f" add upt to more than 100.0% (e.g. at 7 tabs) , so be more precise here - tab_w = 1000.0 // nch /10 + tab_w = 0 + if nch > 0: + if 1000.0 // nch > 0: + tab_w = 1000.0 // nch /10 + l = None for l in self.container_tab_titles.children.values(): l.set_size("%.1f%%" % tab_w, "auto") # and make last tab consume the rounding rest, looks better - last_tab_w=100.0-tab_w*(nch-1) - l.set_size("%.1f%%" % last_tab_w, "auto") + if not l is None: + last_tab_w=100.0-tab_w*(nch-1) + l.set_size("%.1f%%" % last_tab_w, "auto") def append(self, widget, key=''): diff --git a/setup.py b/setup.py index 6d6bd20e..ffcda9aa 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,5 @@ except: del params['setup_requires'] params['use_scm_version'] = False - params['version'] = '2022.03.07' + params['version'] = '2022.7.27' setup(**params) From f7254678abf67609e047e1d4f2d7117052f2ab0c Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 27 Jul 2022 10:38:49 +0200 Subject: [PATCH 077/110] BugFix editor filename uncomplete when enter is pressed during filename editing. --- editor/editor_widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index 633c7ff2..70b5c051 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -429,6 +429,7 @@ def get_fileinput_value(self): def on_enter_key_pressed(self, widget, value, keycode): if keycode == "13": + self.get_field('filename').set_value(value) self.confirm_value(None) @gui.decorate_event From 06808ad6c332f2a4cb3ed33fa834474c66e2d7d9 Mon Sep 17 00:00:00 2001 From: Vincent Poulailleau Date: Thu, 13 Oct 2022 16:20:01 +0200 Subject: [PATCH 078/110] typo (#500) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc870174..ea328b04 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Do you need support? There is also a **drag n drop GUI Editor**. Look at the [Editor](https://github.com/rawpython/remi/tree/master/editor) subfolder to download your copy. -A demostrative video from the great REVVEN labs +A demonstrative video from the great REVVEN labs

From 889be9290ba5c38af1b3f2311af4f8f50431dbba Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 16 Nov 2022 09:57:52 +0100 Subject: [PATCH 079/110] An example of CFC editor. --- examples/others/process_app.py | 347 +++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 examples/others/process_app.py diff --git a/examples/others/process_app.py b/examples/others/process_app.py new file mode 100644 index 00000000..2b9e1ad2 --- /dev/null +++ b/examples/others/process_app.py @@ -0,0 +1,347 @@ +""" + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import remi.gui as gui +import remi.server +from remi import start, App +import os #for path handling + + +class MixinPositionSize(): + def get_position(self): + return float(self.attr_x), float(self.attr_y) + + def get_size(self): + return float(self.attr_width), float(self.attr_height) + + +class MoveableWidget(gui.EventSource, MixinPositionSize): + container = None + def __init__(self, container, *args, **kwargs): + gui.EventSource.__init__(self) + self.container = container + self.active = False + self.onmousedown.do(self.start_drag, js_stop_propagation=True, js_prevent_default=True) + + def start_drag(self, emitter, x, y): + self.active = True + self.container.onmousemove.do(self.on_drag, js_stop_propagation=True, js_prevent_default=True) + self.container.onmouseup.do(self.stop_drag) + self.container.onmouseleave.do(self.stop_drag, 0, 0) + + @gui.decorate_event + def stop_drag(self, emitter, x, y): + self.active = False + return (x, y) + + @gui.decorate_event + def on_drag(self, emitter, x, y): + if self.active: + self.set_position(float(x) - float(self.attr_width)/2.0, float(y) - float(self.attr_height)/2.0) + return (x, y) + + +class Input(): + name = None + typ = None + source = None #has to be an Output + + def __init__(self, name, typ = None): + self.name = name + self.typ = typ + + def get_value(self): + return self.source.get_value() + + def link(self, output): + self.source = output + + +class Output(): + name = None + typ = None + destination = None #has to be an Input + value = None + + def __init__(self, name, typ = None): + self.name = name + self.typ = typ + + def get_value(self): + return self.value + + def link(self, input): + self.destination = input + + +class Element(): + name = None + inputs = None + outputs = None + def __init__(self, name): + self.name = name + self.inputs = [] + self.outputs = [] + + def add_io(self, io): + if issubclass(type(io), Input): + self.inputs.append(io) + else: + self.outputs.append(io) + + +class InputWidget(Input, gui.SvgSubcontainer, MixinPositionSize): + placeholder = None + label = None + def __init__(self, name, *args, **kwargs): + width = 10 * len(name) + height = 20 + gui.SvgSubcontainer.__init__(self, 0, 0, width, height, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, width, height) + self.placeholder.set_stroke(1, 'black') + self.placeholder.set_fill("white") + self.append(self.placeholder) + + self.label = gui.SvgText("0%", "50%", name) + self.label.attr_dominant_baseline = 'middle' + self.label.attr_text_anchor = "start" + self.append(self.label) + + Input.__init__(self, name, "") + + def set_size(self, width, height): + if self.placeholder: + self.placeholder.set_size(width, height) + return super().set_size(width, height) + + @gui.decorate_event + def onpositionchanged(self): + return () + + +class OutputWidget(Output, gui.SvgSubcontainer, MixinPositionSize): + placeholder = None + label = None + def __init__(self, name, *args, **kwargs): + width = 10 * len(name) + height = 20 + gui.SvgSubcontainer.__init__(self, 0, 0, width, height, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, width, height) + self.placeholder.set_stroke(1, 'black') + self.placeholder.set_fill("white") + self.append(self.placeholder) + + self.label = gui.SvgText("100%", "50%", name) + self.label.attr_dominant_baseline = 'middle' + self.label.attr_text_anchor = "end" + self.append(self.label) + + Output.__init__(self, name, "") + + def set_size(self, width, height): + if self.placeholder: + self.placeholder.set_size(width, height) + return super().set_size(width, height) + + @gui.decorate_event + def onpositionchanged(self): + return () + + +class Link(gui.SvgPolyline): + source = None + destination = None + def __init__(self, source_widget, destination_widget, *args, **kwargs): + gui.SvgPolyline.__init__(self, 2, *args, **kwargs) + self.set_stroke(1, 'black') + self.set_fill('transparent') + self.attributes['stroke-dasharray'] = "4 2" + self.source = source_widget + self.source.onpositionchanged.do(self.update_path) + self.destination = destination_widget + self.destination.onpositionchanged.do(self.update_path) + + self.source.destinaton = self.destination + self.destination.source = self.source + self.update_path() + + def update_path(self, emitter=None): + self.attributes['points'] = '' + + x,y = self.source.get_position() + w,h = self.source.get_size() + xsource_parent, ysource_parent = self.source._parent.get_position() + wsource_parent, hsource_parent = self.destination._parent.get_size() + + xsource = xsource_parent + wsource_parent + ysource = ysource_parent + y + h/2.0 + self.add_coord(xsource, ysource) + + x,y = self.destination.get_position() + w,h = self.destination.get_size() + xdestination_parent, ydestination_parent = self.destination._parent.get_position() + wdestination_parent, hdestination_parent = self.destination._parent.get_size() + + xdestination = xdestination_parent + ydestination = ydestination_parent + y + h/2.0 + + offset = 10 + + if xdestination - xsource < offset*2: + self.maxlen = 6 + """ + [ source]---, + | + __________| + | + '----[destination ] + """ + self.add_coord(xsource + offset, ysource) + + if ydestination > ysource: + #self.add_coord(xsource + offset, ysource + (ydestination - ysource)/2.0) + #self.add_coord(xdestination - offset, ysource + (ydestination - ysource)/2.0) + self.add_coord(xsource + offset, (ysource_parent + hsource_parent) + (ydestination_parent - (ysource_parent + hsource_parent))/2.0) + self.add_coord(xdestination - offset, (ysource_parent + hsource_parent) + (ydestination_parent - (ysource_parent + hsource_parent))/2.0) + else: + self.add_coord(xsource + offset, (ydestination_parent + hdestination_parent) + (ysource_parent - (ydestination_parent + hdestination_parent))/2.0) + self.add_coord(xdestination - offset, (ydestination_parent + hdestination_parent) + (ysource_parent - (ydestination_parent + hdestination_parent))/2.0) + self.add_coord(xdestination - offset, ydestination) + + else: + self.maxlen = 4 + """ + [ source]---, + | + '------[destination ] + """ + self.add_coord(xsource + (xdestination-xsource)/2.0, ysource) + self.add_coord(xdestination - (xdestination-xsource)/2.0, ydestination) + + self.add_coord(xdestination, ydestination) + + +class ElementWidget(Element, gui.SvgSubcontainer, MoveableWidget): + + label = None + outline = None + + def __init__(self, name, container, x, y, w, h, *args, **kwargs): + gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) + MoveableWidget.__init__(self, container, *args, **kwargs) + Element.__init__(self, name) + + self.outline = gui.SvgRectangle(0, 0, w, h) + self.outline.set_fill('lightyellow') + self.outline.set_stroke(2, 'black') + self.append(self.outline) + + self.label = gui.SvgText("50%", 0, self.name) + self.label.attr_text_anchor = "middle" + self.label.attr_dominant_baseline = 'hanging' + self.append(self.label) + + def add_io_widget(self, widget): + Element.add_io(self, widget) + self.append(widget) + widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) + widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) + + w, h = self.get_size() + w_width, w_height = widget.get_size() + + i = 1 + for inp in self.inputs: + inp.set_position(0, (h/(len(self.inputs)+1))*i-w_height/2.0) + i += 1 + + i = 1 + for o in self.outputs: + o.set_position(w - w_width, (h/(len(self.outputs)+1))*i-w_height/2.0) + i += 1 + + def set_position(self, x, y): + if self.inputs != None: + for inp in self.inputs: + inp.onpositionchanged() + + for o in self.outputs: + o.onpositionchanged() + return super().set_position(x, y) + + +class ProcessContainer(gui.Svg): + selected_input = None + selected_output = None + + def __init__(self, *args, **kwargs): + gui.Svg.__init__(self, *args, **kwargs) + self.css_border_color = 'black' + self.css_border_width = '1' + self.css_border_style = 'solid' + self.style['background-color'] = 'lightyellow' + + def onselection_start(self, emitter, x, y): + self.selected_input = self.selected_output = None + print("selection start: ", type(emitter)) + if type(emitter) == InputWidget: + self.selected_input = emitter + else: + self.selected_output = emitter + + def onselection_end(self, emitter, x, y): + print("selection end: ", type(emitter)) + if type(emitter) == InputWidget: + self.selected_input = emitter + else: + self.selected_output = emitter + + if self.selected_input != None and self.selected_output != None: + link = Link(self.selected_output, self.selected_input) + self.append(link) + + +class MyApp(App): + def __init__(self, *args): + res_path = os.path.join(os.path.dirname(__file__), 'res') + super(MyApp, self).__init__(*args, static_file_path=res_path) + + def idle(self): + pass + + def main(self): + self.main_container = gui.VBox(width=800, height=800, margin='0px auto') + + self.process_container = ProcessContainer(width=600, height=600) + self.main_container.append(self.process_container) + + m = ElementWidget("Element widget 0", self.process_container, 100, 100, 200, 100) + m.add_io_widget(InputWidget("input0")) + m.add_io_widget(OutputWidget("output0")) + m.add_io_widget(OutputWidget("output1")) + self.process_container.append(m) + + m = ElementWidget("Element widget 1", self.process_container, 100, 100, 200, 100) + m.add_io_widget(InputWidget("input0")) + m.add_io_widget(InputWidget("input1")) + m.add_io_widget(OutputWidget("output0")) + self.process_container.append(m) + + # returning the root widget + return self.main_container + + + +if __name__ == "__main__": + start(MyApp, debug=False, address='0.0.0.0', port=0, update_interval=0.01) From 7b03ee0411325d6f13ed8df5a2cb7cc499b20f6d Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 16 Nov 2022 16:57:28 +0100 Subject: [PATCH 080/110] Process example changes. --- examples/others/process_app.py | 113 +++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/examples/others/process_app.py b/examples/others/process_app.py index 2b9e1ad2..d013496d 100644 --- a/examples/others/process_app.py +++ b/examples/others/process_app.py @@ -16,6 +16,7 @@ import remi.server from remi import start, App import os #for path handling +import inspect class MixinPositionSize(): @@ -89,16 +90,32 @@ class Element(): name = None inputs = None outputs = None + + def decorate_process(output_list): + """ setup a method as a process Element """ + """ + input parameters can be obtained by introspection + outputs values (return values) are to be described with decorator + """ + def add_annotation(method): + setattr(method, "_outputs", output_list) + return method + return add_annotation + def __init__(self, name): self.name = name - self.inputs = [] - self.outputs = [] + self.inputs = {} + self.outputs = {} def add_io(self, io): if issubclass(type(io), Input): - self.inputs.append(io) + self.inputs[io.name] = io else: - self.outputs.append(io) + self.outputs[io.name] = io + + @decorate_process([]) + def do(self): + return True, 28 class InputWidget(Input, gui.SvgSubcontainer, MixinPositionSize): @@ -110,7 +127,7 @@ def __init__(self, name, *args, **kwargs): gui.SvgSubcontainer.__init__(self, 0, 0, width, height, *args, **kwargs) self.placeholder = gui.SvgRectangle(0, 0, width, height) self.placeholder.set_stroke(1, 'black') - self.placeholder.set_fill("white") + self.placeholder.set_fill("lightgray") self.append(self.placeholder) self.label = gui.SvgText("0%", "50%", name) @@ -139,7 +156,7 @@ def __init__(self, name, *args, **kwargs): gui.SvgSubcontainer.__init__(self, 0, 0, width, height, *args, **kwargs) self.placeholder = gui.SvgRectangle(0, 0, width, height) self.placeholder.set_stroke(1, 'black') - self.placeholder.set_fill("white") + self.placeholder.set_fill("lightgray") self.append(self.placeholder) self.label = gui.SvgText("100%", "50%", name) @@ -242,8 +259,8 @@ def __init__(self, name, container, x, y, w, h, *args, **kwargs): MoveableWidget.__init__(self, container, *args, **kwargs) Element.__init__(self, name) - self.outline = gui.SvgRectangle(0, 0, w, h) - self.outline.set_fill('lightyellow') + self.outline = gui.SvgRectangle(0, 0, "100%", "100%") + self.outline.set_fill('white') self.outline.set_stroke(2, 'black') self.append(self.outline) @@ -252,31 +269,44 @@ def __init__(self, name, container, x, y, w, h, *args, **kwargs): self.label.attr_dominant_baseline = 'hanging' self.append(self.label) + #for all the outputs defined by decorator on Element.do + # add the related Outputs + for o in self.do._outputs: + self.add_io_widget(OutputWidget(o)) + + signature = inspect.signature(self.do) + for arg in signature.parameters: + self.add_io_widget(InputWidget(arg)) + def add_io_widget(self, widget): Element.add_io(self, widget) self.append(widget) widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) - w, h = self.get_size() w_width, w_height = widget.get_size() + w, h = self.get_size() + h = w_height * (max(len(self.outputs), len(self.inputs))+2) + gui._MixinSvgSize.set_size(self, w, h) i = 1 - for inp in self.inputs: + for inp in self.inputs.values(): + w_width, w_height = inp.get_size() inp.set_position(0, (h/(len(self.inputs)+1))*i-w_height/2.0) i += 1 i = 1 - for o in self.outputs: + for o in self.outputs.values(): + w_width, w_height = o.get_size() o.set_position(w - w_width, (h/(len(self.outputs)+1))*i-w_height/2.0) i += 1 def set_position(self, x, y): if self.inputs != None: - for inp in self.inputs: + for inp in self.inputs.values(): inp.onpositionchanged() - for o in self.outputs: + for o in self.outputs.values(): o.onpositionchanged() return super().set_position(x, y) @@ -312,6 +342,39 @@ def onselection_end(self, emitter, x, y): self.append(link) +class BOOL(ElementWidget): + actual_value = False + + def __init__(self, name, initial_value, *args, **kwargs): + ElementWidget.__init__(self, name, *args, **kwargs) + self.actual_value = initial_value + self.outputs['OUT'].label.set_fill('white') + self.outputs['OUT'].placeholder.set_fill('blue' if self.actual_value else 'BLACK') + + @Element.decorate_process(['OUT']) + def do(self): + OUT = self.actual_value + return OUT + +class NOT(ElementWidget): + @Element.decorate_process(['OUT']) + def do(self, IN): + OUT = not IN + return OUT + +class AND(ElementWidget): + @Element.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 and IN2 + return OUT + +class OR(ElementWidget): + @Element.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 and IN2 + return OUT + + class MyApp(App): def __init__(self, *args): res_path = os.path.join(os.path.dirname(__file__), 'res') @@ -326,16 +389,24 @@ def main(self): self.process_container = ProcessContainer(width=600, height=600) self.main_container.append(self.process_container) - m = ElementWidget("Element widget 0", self.process_container, 100, 100, 200, 100) - m.add_io_widget(InputWidget("input0")) - m.add_io_widget(OutputWidget("output0")) - m.add_io_widget(OutputWidget("output1")) + y = 10 + m = BOOL("BOOL", False, self.process_container, 100, y, 200, 100) + self.process_container.append(m) + + y += 110 + m = BOOL("BOOL 2", True, self.process_container, 100, y, 200, 100) + self.process_container.append(m) + + y += 110 + m = NOT("NOT 0", self.process_container, 100, y, 200, 100) + self.process_container.append(m) + + y += 110 + m = AND("AND", self.process_container, 100, y, 200, 100) self.process_container.append(m) - m = ElementWidget("Element widget 1", self.process_container, 100, 100, 200, 100) - m.add_io_widget(InputWidget("input0")) - m.add_io_widget(InputWidget("input1")) - m.add_io_widget(OutputWidget("output0")) + y += 110 + m = OR("OR", self.process_container, 100, y, 200, 100) self.process_container.append(m) # returning the root widget From d9cd7801c4ba2cd11fde848db3ed8f1b055ddb50 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 16 Nov 2022 17:54:02 +0100 Subject: [PATCH 081/110] Process example changes. --- examples/others/process_app.py | 129 ++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 41 deletions(-) diff --git a/examples/others/process_app.py b/examples/others/process_app.py index d013496d..cc9cf995 100644 --- a/examples/others/process_app.py +++ b/examples/others/process_app.py @@ -82,17 +82,20 @@ def __init__(self, name, typ = None): def get_value(self): return self.value + def set_value(self, value): + self.value = value + def link(self, input): self.destination = input -class Element(): +class Subprocess(): name = None inputs = None outputs = None def decorate_process(output_list): - """ setup a method as a process Element """ + """ setup a method as a process Subprocess """ """ input parameters can be obtained by introspection outputs values (return values) are to be described with decorator @@ -112,13 +115,13 @@ def add_io(self, io): self.inputs[io.name] = io else: self.outputs[io.name] = io - + @decorate_process([]) def do(self): return True, 28 -class InputWidget(Input, gui.SvgSubcontainer, MixinPositionSize): +class InputView(Input, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None def __init__(self, name, *args, **kwargs): @@ -147,7 +150,7 @@ def onpositionchanged(self): return () -class OutputWidget(Output, gui.SvgSubcontainer, MixinPositionSize): +class OutputView(Output, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None def __init__(self, name, *args, **kwargs): @@ -171,6 +174,15 @@ def set_size(self, width, height): self.placeholder.set_size(width, height) return super().set_size(width, height) + def set_value(self, value): + if type(value) == bool: + self.label.set_fill('white') + self.placeholder.set_fill('blue' if value else 'BLACK') + else: + self.label.set_text(self.name + " : " + str(value)) + + Output.set_value(self, value) + @gui.decorate_event def onpositionchanged(self): return () @@ -249,7 +261,7 @@ def update_path(self, emitter=None): self.add_coord(xdestination, ydestination) -class ElementWidget(Element, gui.SvgSubcontainer, MoveableWidget): +class SubprocessView(Subprocess, gui.SvgSubcontainer, MoveableWidget): label = None outline = None @@ -257,7 +269,7 @@ class ElementWidget(Element, gui.SvgSubcontainer, MoveableWidget): def __init__(self, name, container, x, y, w, h, *args, **kwargs): gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) MoveableWidget.__init__(self, container, *args, **kwargs) - Element.__init__(self, name) + Subprocess.__init__(self, name) self.outline = gui.SvgRectangle(0, 0, "100%", "100%") self.outline.set_fill('white') @@ -269,17 +281,17 @@ def __init__(self, name, container, x, y, w, h, *args, **kwargs): self.label.attr_dominant_baseline = 'hanging' self.append(self.label) - #for all the outputs defined by decorator on Element.do + #for all the outputs defined by decorator on Subprocess.do # add the related Outputs for o in self.do._outputs: - self.add_io_widget(OutputWidget(o)) + self.add_io_widget(OutputView(o)) signature = inspect.signature(self.do) for arg in signature.parameters: - self.add_io_widget(InputWidget(arg)) + self.add_io_widget(InputView(arg)) def add_io_widget(self, widget): - Element.add_io(self, widget) + Subprocess.add_io(self, widget) self.append(widget) widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) @@ -311,12 +323,43 @@ def set_position(self, x, y): return super().set_position(x, y) -class ProcessContainer(gui.Svg): +class Process(): + subprocesses = None + def __init__(self): + self.subprocesses = {} + + def add_subprocess(self, subprocess): + self.subprocesses[subprocess.name] = subprocess + + def do(self): + for subprocesses in self.subprocesses.values(): + parameters = {} + all_inputs_connected = True + for IN in subprocesses.inputs.values(): + if IN.source == None: + all_inputs_connected = False + continue + parameters[IN.name] = IN.get_value() + + if not all_inputs_connected: + return + output_results = subprocesses.do(**parameters) + i = 0 + for OUT in subprocesses.outputs.values(): + if type(output_results) in (tuple, list): + OUT.set_value(output_results[i]) + else: + OUT.set_value(output_results) + i += 1 + + +class ProcessView(gui.Svg, Process): selected_input = None selected_output = None def __init__(self, *args, **kwargs): gui.Svg.__init__(self, *args, **kwargs) + Process.__init__(self) self.css_border_color = 'black' self.css_border_width = '1' self.css_border_style = 'solid' @@ -325,14 +368,14 @@ def __init__(self, *args, **kwargs): def onselection_start(self, emitter, x, y): self.selected_input = self.selected_output = None print("selection start: ", type(emitter)) - if type(emitter) == InputWidget: + if type(emitter) == InputView: self.selected_input = emitter else: self.selected_output = emitter def onselection_end(self, emitter, x, y): print("selection end: ", type(emitter)) - if type(emitter) == InputWidget: + if type(emitter) == InputView: self.selected_input = emitter else: self.selected_output = emitter @@ -341,73 +384,77 @@ def onselection_end(self, emitter, x, y): link = Link(self.selected_output, self.selected_input) self.append(link) + def add_subprocess(self, subprocess): + self.append(subprocess) + Process.add_subprocess(self, subprocess) -class BOOL(ElementWidget): - actual_value = False +class BOOL(SubprocessView): def __init__(self, name, initial_value, *args, **kwargs): - ElementWidget.__init__(self, name, *args, **kwargs) - self.actual_value = initial_value - self.outputs['OUT'].label.set_fill('white') - self.outputs['OUT'].placeholder.set_fill('blue' if self.actual_value else 'BLACK') + SubprocessView.__init__(self, name, *args, **kwargs) + self.outputs['OUT'].set_value(initial_value) - @Element.decorate_process(['OUT']) + @Subprocess.decorate_process(['OUT']) def do(self): - OUT = self.actual_value + OUT = self.outputs['OUT'].get_value() return OUT -class NOT(ElementWidget): - @Element.decorate_process(['OUT']) +class NOT(SubprocessView): + @Subprocess.decorate_process(['OUT']) def do(self, IN): OUT = not IN return OUT -class AND(ElementWidget): - @Element.decorate_process(['OUT']) +class AND(SubprocessView): + @Subprocess.decorate_process(['OUT']) def do(self, IN1, IN2): OUT = IN1 and IN2 return OUT -class OR(ElementWidget): - @Element.decorate_process(['OUT']) +class OR(SubprocessView): + @Subprocess.decorate_process(['OUT']) def do(self, IN1, IN2): OUT = IN1 and IN2 return OUT class MyApp(App): + process = None + def __init__(self, *args): res_path = os.path.join(os.path.dirname(__file__), 'res') super(MyApp, self).__init__(*args, static_file_path=res_path) def idle(self): - pass + if self.process is None: + return + self.process.do() def main(self): self.main_container = gui.VBox(width=800, height=800, margin='0px auto') - self.process_container = ProcessContainer(width=600, height=600) - self.main_container.append(self.process_container) + self.process = ProcessView(width=600, height=600) + self.main_container.append(self.process) y = 10 - m = BOOL("BOOL", False, self.process_container, 100, y, 200, 100) - self.process_container.append(m) + m = BOOL("BOOL", False, self.process, 100, y, 200, 100) + self.process.add_subprocess(m) y += 110 - m = BOOL("BOOL 2", True, self.process_container, 100, y, 200, 100) - self.process_container.append(m) + m = BOOL("BOOL 2", True, self.process, 100, y, 200, 100) + self.process.add_subprocess(m) y += 110 - m = NOT("NOT 0", self.process_container, 100, y, 200, 100) - self.process_container.append(m) + m = NOT("NOT 0", self.process, 100, y, 200, 100) + self.process.add_subprocess(m) y += 110 - m = AND("AND", self.process_container, 100, y, 200, 100) - self.process_container.append(m) + m = AND("AND", self.process, 100, y, 200, 100) + self.process.add_subprocess(m) y += 110 - m = OR("OR", self.process_container, 100, y, 200, 100) - self.process_container.append(m) + m = OR("OR", self.process, 100, y, 200, 100) + self.process.add_subprocess(m) # returning the root widget return self.main_container From e591bef9bc67238730db9fe6457381dc12baff64 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 16 Nov 2022 17:57:55 +0100 Subject: [PATCH 082/110] Process example changes. --- examples/others/process_app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/others/process_app.py b/examples/others/process_app.py index cc9cf995..229c14bf 100644 --- a/examples/others/process_app.py +++ b/examples/others/process_app.py @@ -68,6 +68,9 @@ def get_value(self): def link(self, output): self.source = output + def is_linked(self): + return self.source != None + class Output(): name = None @@ -87,6 +90,9 @@ def set_value(self, value): def link(self, input): self.destination = input + + def is_linked(self): + return self.destination != None class Subprocess(): @@ -381,6 +387,8 @@ def onselection_end(self, emitter, x, y): self.selected_output = emitter if self.selected_input != None and self.selected_output != None: + if self.selected_input.is_linked(): + return link = Link(self.selected_output, self.selected_input) self.append(link) From 005d6b0183a695388c2d57f77c2c2d8837eab3e1 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 17 Nov 2022 12:49:49 +0100 Subject: [PATCH 083/110] Process example changes. --- examples/others/process_app.py | 217 +++++++++++++++++++++++++++------ 1 file changed, 182 insertions(+), 35 deletions(-) diff --git a/examples/others/process_app.py b/examples/others/process_app.py index 229c14bf..dd6b0006 100644 --- a/examples/others/process_app.py +++ b/examples/others/process_app.py @@ -17,6 +17,7 @@ from remi import start, App import os #for path handling import inspect +import time class MixinPositionSize(): @@ -127,21 +128,28 @@ def do(self): return True, 28 +class SvgTitle(gui.Widget, gui._MixinTextualWidget): + def __init__(self, text='svg text', *args, **kwargs): + super(SvgTitle, self).__init__(*args, **kwargs) + self.type = 'title' + self.set_text(text) + + class InputView(Input, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None def __init__(self, name, *args, **kwargs): - width = 10 * len(name) - height = 20 - gui.SvgSubcontainer.__init__(self, 0, 0, width, height, *args, **kwargs) - self.placeholder = gui.SvgRectangle(0, 0, width, height) + gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, 0, 0) self.placeholder.set_stroke(1, 'black') self.placeholder.set_fill("lightgray") + self.placeholder.style['cursor'] = 'pointer' self.append(self.placeholder) self.label = gui.SvgText("0%", "50%", name) self.label.attr_dominant_baseline = 'middle' self.label.attr_text_anchor = "start" + self.label.style['cursor'] = 'pointer' self.append(self.label) Input.__init__(self, name, "") @@ -149,7 +157,7 @@ def __init__(self, name, *args, **kwargs): def set_size(self, width, height): if self.placeholder: self.placeholder.set_size(width, height) - return super().set_size(width, height) + return gui._MixinSvgSize.set_size(self, width, height) @gui.decorate_event def onpositionchanged(self): @@ -160,17 +168,17 @@ class OutputView(Output, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None def __init__(self, name, *args, **kwargs): - width = 10 * len(name) - height = 20 - gui.SvgSubcontainer.__init__(self, 0, 0, width, height, *args, **kwargs) - self.placeholder = gui.SvgRectangle(0, 0, width, height) + gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, 0, 0) self.placeholder.set_stroke(1, 'black') self.placeholder.set_fill("lightgray") + self.placeholder.style['cursor'] = 'pointer' self.append(self.placeholder) self.label = gui.SvgText("100%", "50%", name) self.label.attr_dominant_baseline = 'middle' self.label.attr_text_anchor = "end" + self.label.style['cursor'] = 'pointer' self.append(self.label) Output.__init__(self, name, "") @@ -178,14 +186,18 @@ def __init__(self, name, *args, **kwargs): def set_size(self, width, height): if self.placeholder: self.placeholder.set_size(width, height) - return super().set_size(width, height) + return gui._MixinSvgSize.set_size(self, width, height) def set_value(self, value): + if value == self.value: + return if type(value) == bool: self.label.set_fill('white') self.placeholder.set_fill('blue' if value else 'BLACK') - else: - self.label.set_text(self.name + " : " + str(value)) + + self.append(SvgTitle(str(value)), "title") + + self.label.attr_title = str(value) Output.set_value(self, value) @@ -217,7 +229,7 @@ def update_path(self, emitter=None): x,y = self.source.get_position() w,h = self.source.get_size() xsource_parent, ysource_parent = self.source._parent.get_position() - wsource_parent, hsource_parent = self.destination._parent.get_size() + wsource_parent, hsource_parent = self.source._parent.get_size() xsource = xsource_parent + wsource_parent ysource = ysource_parent + y + h/2.0 @@ -270,12 +282,17 @@ def update_path(self, emitter=None): class SubprocessView(Subprocess, gui.SvgSubcontainer, MoveableWidget): label = None + label_font_size = 12 + outline = None - def __init__(self, name, container, x, y, w, h, *args, **kwargs): - gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) - MoveableWidget.__init__(self, container, *args, **kwargs) + io_font_size = 12 + io_left_right_offset = 10 + + def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): Subprocess.__init__(self, name) + gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) + MoveableWidget.__init__(self, container, *args, **kwargs) self.outline = gui.SvgRectangle(0, 0, "100%", "100%") self.outline.set_fill('white') @@ -285,6 +302,7 @@ def __init__(self, name, container, x, y, w, h, *args, **kwargs): self.label = gui.SvgText("50%", 0, self.name) self.label.attr_text_anchor = "middle" self.label.attr_dominant_baseline = 'hanging' + self.label.css_font_size = gui.to_pix(self.label_font_size) self.append(self.label) #for all the outputs defined by decorator on Subprocess.do @@ -296,27 +314,48 @@ def __init__(self, name, container, x, y, w, h, *args, **kwargs): for arg in signature.parameters: self.add_io_widget(InputView(arg)) + def calc_height(self): + inputs_count = 0 if self.inputs == None else len(self.inputs) + outputs_count = 0 if self.outputs == None else len(self.outputs) + return self.label_font_size + (max(outputs_count, inputs_count)+2) * self.io_font_size + + def calc_width(self): + max_name_len_input = 0 + if self.inputs != None: + for inp in self.inputs.values(): + max_name_len_input = max(max_name_len_input, len(inp.name)) + + max_name_len_output = 0 + if self.outputs != None: + for o in self.outputs.values(): + max_name_len_output = max(max_name_len_output, len(o.name)) + + return max((len(self.name) * self.label_font_size), (max(max_name_len_input, max_name_len_output)*self.io_font_size) * 2) + self.io_left_right_offset + def add_io_widget(self, widget): + widget.label.css_font_size = gui.to_pix(self.io_font_size) + widget.set_size(len(widget.name) * self.io_font_size, self.io_font_size) + Subprocess.add_io(self, widget) self.append(widget) widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) - w_width, w_height = widget.get_size() + self.adjust_geometry() + + def adjust_geometry(self): + gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) w, h = self.get_size() - h = w_height * (max(len(self.outputs), len(self.inputs))+2) - gui._MixinSvgSize.set_size(self, w, h) i = 1 for inp in self.inputs.values(): - w_width, w_height = inp.get_size() - inp.set_position(0, (h/(len(self.inputs)+1))*i-w_height/2.0) + inp.set_position(0, self.label_font_size + self.io_font_size*i) i += 1 i = 1 for o in self.outputs.values(): - w_width, w_height = o.get_size() - o.set_position(w - w_width, (h/(len(self.outputs)+1))*i-w_height/2.0) + ow, oh = o.get_size() + o.set_position(w - ow, self.label_font_size + self.io_font_size*i) i += 1 def set_position(self, x, y): @@ -328,6 +367,11 @@ def set_position(self, x, y): o.onpositionchanged() return super().set_position(x, y) + def set_name(self, name): + self.name = name + self.label.set_text(name) + self.adjust_geometry() + class Process(): subprocesses = None @@ -348,8 +392,10 @@ def do(self): parameters[IN.name] = IN.get_value() if not all_inputs_connected: - return + continue output_results = subprocesses.do(**parameters) + if output_results is None: + continue i = 0 for OUT in subprocesses.outputs.values(): if type(output_results) in (tuple, list): @@ -397,16 +443,46 @@ def add_subprocess(self, subprocess): Process.add_subprocess(self, subprocess) +class STRING(SubprocessView): + def __init__(self, name, *args, **kwargs): + SubprocessView.__init__(self, name, *args, **kwargs) + self.outputs['OUT'].set_value("A STRING VALUE") + + @Subprocess.decorate_process(['OUT']) + def do(self): + OUT = self.outputs['OUT'].get_value() + return OUT + +class STRING_SWAP_CASE(SubprocessView): + def __init__(self, name, *args, **kwargs): + SubprocessView.__init__(self, name, *args, **kwargs) + + @Subprocess.decorate_process(['OUT']) + def do(self, EN, IN): + if not EN: + return + OUT = IN.swapcase() + return OUT + class BOOL(SubprocessView): - def __init__(self, name, initial_value, *args, **kwargs): + def __init__(self, name, *args, **kwargs): SubprocessView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value(initial_value) + self.outputs['OUT'].set_value(False) @Subprocess.decorate_process(['OUT']) def do(self): OUT = self.outputs['OUT'].get_value() return OUT +class RISING_EDGE(SubprocessView): + previous_value = None + + @Subprocess.decorate_process(['OUT']) + def do(self, IN): + OUT = (self.previous_value != IN) and IN + self.previous_value = IN + return OUT + class NOT(SubprocessView): @Subprocess.decorate_process(['OUT']) def do(self, IN): @@ -422,10 +498,62 @@ def do(self, IN1, IN2): class OR(SubprocessView): @Subprocess.decorate_process(['OUT']) def do(self, IN1, IN2): - OUT = IN1 and IN2 + OUT = IN1 or IN2 + return OUT + +class XOR(SubprocessView): + @Subprocess.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 != IN2 + return OUT + +class PULSAR(SubprocessView): + ton = 1000 + toff = 1000 + tstart = 0 + def __init__(self, name, *args, **kwargs): + SubprocessView.__init__(self, name, *args, **kwargs) + self.outputs['OUT'].set_value(False) + self.tstart = time.time() + + @Subprocess.decorate_process(['OUT']) + def do(self): + OUT = (int((time.time() - self.tstart)*1000) % (self.ton + self.toff)) < self.ton return OUT +class Toolbox(gui.VBox): + process_view = None + + index_added_tool = 0 + + def __init__(self, process_view, *args, **kwargs): + gui.VBox.__init__(self, *args, **kwargs) + self.process_view = process_view + self.append(gui.Label("Toolbox", width="100%", height="auto", style={'outline':'1px solid black'})) + + self.container = gui.VBox(width="100%", height="auto", style={'outline':'1px solid black'}) + self.container.css_justify_content = 'flex-start' + self.container.style['row-gap'] = '10px' + self.append(self.container) + + self.css_justify_content = 'flex-start' + + def add_tool(self, tool_class): + tool_class_widget = gui.Label(tool_class.__name__, style={'outline':'1px solid black'}) + tool_class_widget.tool_class = tool_class + tool_class_widget.onclick.do(self.on_tool_selected) + tool_class_widget.style['cursor'] = 'pointer' + self.container.append(tool_class_widget) + + def on_tool_selected(self, tool_class_widget): + tool_class = tool_class_widget.tool_class + tool_instance = tool_class(tool_class.__name__ , self.process_view) + tool_instance.set_name(tool_instance.name + "_" + str(self.index_added_tool)) + self.index_added_tool += 1 + self.process_view.add_subprocess(tool_instance) + + class MyApp(App): process = None @@ -439,30 +567,49 @@ def idle(self): self.process.do() def main(self): - self.main_container = gui.VBox(width=800, height=800, margin='0px auto') - + self.main_container = gui.AsciiContainer(width=800, height=800, margin='0px auto') + self.main_container.set_from_asciiart( + """ + |toolbox|process_view | + """, 0, 0 + ) + self.process = ProcessView(width=600, height=600) - self.main_container.append(self.process) + self.toolbox = Toolbox(self.process) + self.toolbox.add_tool(BOOL) + self.toolbox.add_tool(NOT) + self.toolbox.add_tool(AND) + self.toolbox.add_tool(OR) + self.toolbox.add_tool(XOR) + self.toolbox.add_tool(PULSAR) + self.toolbox.add_tool(STRING) + self.toolbox.add_tool(STRING_SWAP_CASE) + self.toolbox.add_tool(RISING_EDGE) + + self.main_container.append(self.toolbox, 'toolbox') + self.main_container.append(self.process, 'process_view') + """ y = 10 - m = BOOL("BOOL", False, self.process, 100, y, 200, 100) + m = BOOL("BOOL", False, self.process, 100, y) self.process.add_subprocess(m) y += 110 - m = BOOL("BOOL 2", True, self.process, 100, y, 200, 100) + m = BOOL("BOOL 2", True, self.process, 100, y) self.process.add_subprocess(m) y += 110 - m = NOT("NOT 0", self.process, 100, y, 200, 100) + m = NOT("NOT 0", self.process, 100, y) self.process.add_subprocess(m) y += 110 - m = AND("AND", self.process, 100, y, 200, 100) + m = AND("AND", self.process, 100, y) self.process.add_subprocess(m) y += 110 - m = OR("OR", self.process, 100, y, 200, 100) + m = OR("OR", self.process, 100, y) self.process.add_subprocess(m) + """ # returning the root widget return self.main_container From 96e42d268489758f7033695f9a025ad2b9f36a5d Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 17 Nov 2022 14:45:46 +0100 Subject: [PATCH 084/110] Moved process_app to FBD script. Thinking about integrating it in the editor. --- .../others/process_app.py => editor/FBD.py | 40 +++++++++++++++++-- editor/editor_widgets.py | 2 +- 2 files changed, 37 insertions(+), 5 deletions(-) rename examples/others/process_app.py => editor/FBD.py (92%) diff --git a/examples/others/process_app.py b/editor/FBD.py similarity index 92% rename from examples/others/process_app.py rename to editor/FBD.py index dd6b0006..dab32ca6 100644 --- a/examples/others/process_app.py +++ b/editor/FBD.py @@ -18,6 +18,7 @@ import os #for path handling import inspect import time +from editor_widgets import * class MixinPositionSize(): @@ -439,11 +440,25 @@ def onselection_end(self, emitter, x, y): self.append(link) def add_subprocess(self, subprocess): + subprocess.onclick.do(self.onsubprocess_clicked) self.append(subprocess) Process.add_subprocess(self, subprocess) + @gui.decorate_event + def onsubprocess_clicked(self, subprocess): + return (subprocess,) + class STRING(SubprocessView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', str, {}) + def value(self): + if len(self.outputs) < 1: + return "" + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + def __init__(self, name, *args, **kwargs): SubprocessView.__init__(self, name, *args, **kwargs) self.outputs['OUT'].set_value("A STRING VALUE") @@ -465,6 +480,15 @@ def do(self, EN, IN): return OUT class BOOL(SubprocessView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', bool, {}) + def value(self): + if len(self.outputs) < 1: + return False + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + def __init__(self, name, *args, **kwargs): SubprocessView.__init__(self, name, *args, **kwargs) self.outputs['OUT'].set_value(False) @@ -556,10 +580,13 @@ def on_tool_selected(self, tool_class_widget): class MyApp(App): process = None + toolbox = None + attributes_editor = None def __init__(self, *args): - res_path = os.path.join(os.path.dirname(__file__), 'res') - super(MyApp, self).__init__(*args, static_file_path=res_path) + editor_res_path = os.path.join(os.path.dirname(__file__), 'res') + super(MyApp, self).__init__( + *args, static_file_path={'editor_resources': editor_res_path}) def idle(self): if self.process is None: @@ -567,14 +594,16 @@ def idle(self): self.process.do() def main(self): - self.main_container = gui.AsciiContainer(width=800, height=800, margin='0px auto') + self.main_container = gui.AsciiContainer(width="100%", height="100%", margin='0px auto') self.main_container.set_from_asciiart( """ - |toolbox|process_view | + |toolbox|process_view |attributes| """, 0, 0 ) self.process = ProcessView(width=600, height=600) + self.process.onsubprocess_clicked.do(self.onprocessview_subprocess_clicked) + self.attributes_editor = EditorAttributes(self) self.toolbox = Toolbox(self.process) self.toolbox.add_tool(BOOL) self.toolbox.add_tool(NOT) @@ -588,6 +617,7 @@ def main(self): self.main_container.append(self.toolbox, 'toolbox') self.main_container.append(self.process, 'process_view') + self.main_container.append(self.attributes_editor, 'attributes') """ y = 10 @@ -614,6 +644,8 @@ def main(self): # returning the root widget return self.main_container + def onprocessview_subprocess_clicked(self, emitter, subprocess): + self.attributes_editor.set_widget(subprocess) if __name__ == "__main__": diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index 70b5c051..3897eb84 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -779,7 +779,7 @@ def set_widget(self, widget): for w in self.attributesInputs: if w.attributeDict['group'] in self.attributeGroups: - self.attributeGroups[w.attributeDict['group']].attributes_groups_container.remove_child(w) + self.attributeGroups[w.attributeDict['group']].remove_child(w) index = 100 default_width = "100%" From 3d320de6665ea4a679a1e916f337b300d500b765 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Thu, 17 Nov 2022 18:00:06 +0100 Subject: [PATCH 085/110] Function Block Diagram. --- editor/FBD_library.py | 103 ++++++++ editor/FBD_model.py | 133 ++++++++++ editor/{FBD.py => FBD_view.py} | 469 +++++++++++++-------------------- 3 files changed, 415 insertions(+), 290 deletions(-) create mode 100644 editor/FBD_library.py create mode 100644 editor/FBD_model.py rename editor/{FBD.py => FBD_view.py} (56%) diff --git a/editor/FBD_library.py b/editor/FBD_library.py new file mode 100644 index 00000000..ad522145 --- /dev/null +++ b/editor/FBD_library.py @@ -0,0 +1,103 @@ + +import FBD_view +import FBD_model +import remi +import remi.gui as gui +import time + +class STRING(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', str, {}) + def value(self): + if len(self.outputs) < 1: + return "" + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + + def __init__(self, name, *args, **kwargs): + FBD_view.FunctionBlockViewFunctionBlockView.__init__(self, name, *args, **kwargs) + self.outputs['OUT'].set_value("A STRING VALUE") + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = self.outputs['OUT'].get_value() + return OUT + +class STRING_SWAP_CASE(FBD_view.FunctionBlockView): + def __init__(self, name, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN, EN = True): + if not EN: + return + OUT = IN.swapcase() + return OUT + +class BOOL(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', bool, {}) + def value(self): + if len(self.outputs) < 1: + return False + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + + def __init__(self, name, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) + self.outputs['OUT'].set_value(False) + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = self.outputs['OUT'].get_value() + return OUT + +class RISING_EDGE(FBD_view.FunctionBlockView): + previous_value = None + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN): + OUT = (self.previous_value != IN) and IN + self.previous_value = IN + return OUT + +class NOT(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN): + OUT = not IN + return OUT + +class AND(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 and IN2 + return OUT + +class OR(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 or IN2 + return OUT + +class XOR(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 != IN2 + return OUT + +class PULSAR(FBD_view.FunctionBlockView): + ton = 1000 + toff = 1000 + tstart = 0 + def __init__(self, name, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) + self.outputs['OUT'].set_value(False) + self.tstart = time.time() + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = (int((time.time() - self.tstart)*1000) % (self.ton + self.toff)) < self.ton + return OUT + diff --git a/editor/FBD_model.py b/editor/FBD_model.py new file mode 100644 index 00000000..12f57848 --- /dev/null +++ b/editor/FBD_model.py @@ -0,0 +1,133 @@ +import inspect + +class Input(): + name = None + typ = None + source = None #has to be an Output + + def __init__(self, name, typ = None): + self.name = name + self.typ = typ + + def get_value(self): + return self.source.get_value() + + def link(self, output): + self.source = output + + def is_linked(self): + return self.source != None + + def unlink(self): + self.link(None) + + +class Output(): + name = None + typ = None + destination = None #has to be an Input + value = None + + def __init__(self, name, typ = None): + self.name = name + self.typ = typ + + def get_value(self): + return self.value + + def set_value(self, value): + self.value = value + + def link(self, input): + self.destination = input + + def is_linked(self): + return self.destination != None + + def unlink(self): + self.link(None) + + +class FunctionBlock(): + name = None + inputs = None + outputs = None + + def decorate_process(output_list): + """ setup a method as a process FunctionBlock """ + """ + input parameters can be obtained by introspection + outputs values (return values) are to be described with decorator + """ + def add_annotation(method): + setattr(method, "_outputs", output_list) + return method + return add_annotation + + def __init__(self, name): + self.name = name + self.inputs = {} + self.outputs = {} + + def add_io(self, io): + if issubclass(type(io), Input): + self.inputs[io.name] = io + else: + self.outputs[io.name] = io + + @decorate_process([]) + def do(self): + return True, 28 + + +class Link(): + source = None + destination = None + def __init__(self, source_widget, destination_widget): + self.source = source_widget + self.source.onpositionchanged.do(self.update_path) + self.destination = destination_widget + self.destination.onpositionchanged.do(self.update_path) + + self.source.destinaton = self.destination + self.destination.source = self.source + self.update_path() + + def unlink(self): + self.get_parent().remove_child(self.unlink_bt) + self.get_parent().remove_child(self) + + +class Process(): + function_blocks = None + def __init__(self): + self.function_blocks = {} + + def add_function_block(self, function_block): + self.function_blocks[function_block.name] = function_block + + def do(self): + for function_block in self.function_blocks.values(): + parameters = {} + all_inputs_connected = True + + function_block_default_inputs = inspect.signature(function_block.do).parameters + + for IN in function_block.inputs.values(): + if (not IN.is_linked()) and function_block_default_inputs[IN.name].default == inspect.Parameter.empty: + all_inputs_connected = False + continue + parameters[IN.name] = IN.get_value() if IN.is_linked() else function_block_default_inputs[IN.name].default + + if not all_inputs_connected: + continue + output_results = function_block.do(**parameters) + if output_results is None: + continue + i = 0 + for OUT in function_block.outputs.values(): + if type(output_results) in (tuple, list): + OUT.set_value(output_results[i]) + else: + OUT.set_value(output_results) + i += 1 diff --git a/editor/FBD.py b/editor/FBD_view.py similarity index 56% rename from editor/FBD.py rename to editor/FBD_view.py index dab32ca6..a188f5a1 100644 --- a/editor/FBD.py +++ b/editor/FBD_view.py @@ -19,7 +19,7 @@ import inspect import time from editor_widgets import * - +import FBD_model class MixinPositionSize(): def get_position(self): @@ -55,80 +55,6 @@ def on_drag(self, emitter, x, y): return (x, y) -class Input(): - name = None - typ = None - source = None #has to be an Output - - def __init__(self, name, typ = None): - self.name = name - self.typ = typ - - def get_value(self): - return self.source.get_value() - - def link(self, output): - self.source = output - - def is_linked(self): - return self.source != None - - -class Output(): - name = None - typ = None - destination = None #has to be an Input - value = None - - def __init__(self, name, typ = None): - self.name = name - self.typ = typ - - def get_value(self): - return self.value - - def set_value(self, value): - self.value = value - - def link(self, input): - self.destination = input - - def is_linked(self): - return self.destination != None - - -class Subprocess(): - name = None - inputs = None - outputs = None - - def decorate_process(output_list): - """ setup a method as a process Subprocess """ - """ - input parameters can be obtained by introspection - outputs values (return values) are to be described with decorator - """ - def add_annotation(method): - setattr(method, "_outputs", output_list) - return method - return add_annotation - - def __init__(self, name): - self.name = name - self.inputs = {} - self.outputs = {} - - def add_io(self, io): - if issubclass(type(io), Input): - self.inputs[io.name] = io - else: - self.outputs[io.name] = io - - @decorate_process([]) - def do(self): - return True, 28 - - class SvgTitle(gui.Widget, gui._MixinTextualWidget): def __init__(self, text='svg text', *args, **kwargs): super(SvgTitle, self).__init__(*args, **kwargs) @@ -136,7 +62,7 @@ def __init__(self, text='svg text', *args, **kwargs): self.set_text(text) -class InputView(Input, gui.SvgSubcontainer, MixinPositionSize): +class InputView(FBD_model.Input, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None def __init__(self, name, *args, **kwargs): @@ -153,7 +79,7 @@ def __init__(self, name, *args, **kwargs): self.label.style['cursor'] = 'pointer' self.append(self.label) - Input.__init__(self, name, "") + FBD_model.Input.__init__(self, name, "") def set_size(self, width, height): if self.placeholder: @@ -165,7 +91,7 @@ def onpositionchanged(self): return () -class OutputView(Output, gui.SvgSubcontainer, MixinPositionSize): +class OutputView(FBD_model.Output, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None def __init__(self, name, *args, **kwargs): @@ -182,7 +108,7 @@ def __init__(self, name, *args, **kwargs): self.label.style['cursor'] = 'pointer' self.append(self.label) - Output.__init__(self, name, "") + FBD_model.Output.__init__(self, name, "") def set_size(self, width, height): if self.placeholder: @@ -200,37 +126,44 @@ def set_value(self, value): self.label.attr_title = str(value) - Output.set_value(self, value) + FBD_model.Output.set_value(self, value) @gui.decorate_event def onpositionchanged(self): return () -class Link(gui.SvgPolyline): - source = None - destination = None +class LinkView(gui.SvgPolyline, FBD_model.Link): def __init__(self, source_widget, destination_widget, *args, **kwargs): gui.SvgPolyline.__init__(self, 2, *args, **kwargs) + FBD_model.Link.__init__(source_widget, destination_widget) self.set_stroke(1, 'black') self.set_fill('transparent') self.attributes['stroke-dasharray'] = "4 2" - self.source = source_widget - self.source.onpositionchanged.do(self.update_path) - self.destination = destination_widget - self.destination.onpositionchanged.do(self.update_path) - - self.source.destinaton = self.destination - self.destination.source = self.source self.update_path() + self.unlink_bt = gui.SvgSubcontainer(0,0,0,0) + line = gui.SvgLine(0,0,"100%","100%") + line.set_stroke(1, 'red') + self.unlink_bt.append(line) + line = gui.SvgLine("100%", 0, 0, "100%") + line.set_stroke(1, 'red') + self.unlink_bt.append(line) + self.get_parent().append(self.unlink_bt) + self.unlink_bt.onclick.do(self.unlink) + + def unlink(self, emitter): + self.get_parent().remove_child(self.unlink_bt) + self.get_parent().remove_child(self) + FBD_model.Link.unlink() + def update_path(self, emitter=None): self.attributes['points'] = '' x,y = self.source.get_position() w,h = self.source.get_size() - xsource_parent, ysource_parent = self.source._parent.get_position() - wsource_parent, hsource_parent = self.source._parent.get_size() + xsource_parent, ysource_parent = self.source.get_parent().get_position() + wsource_parent, hsource_parent = self.source.get_parent().get_size() xsource = xsource_parent + wsource_parent ysource = ysource_parent + y + h/2.0 @@ -238,8 +171,8 @@ def update_path(self, emitter=None): x,y = self.destination.get_position() w,h = self.destination.get_size() - xdestination_parent, ydestination_parent = self.destination._parent.get_position() - wdestination_parent, hdestination_parent = self.destination._parent.get_size() + xdestination_parent, ydestination_parent = self.destination.get_parent().get_position() + wdestination_parent, hdestination_parent = self.destination.get_parent().get_size() xdestination = xdestination_parent ydestination = ydestination_parent + y + h/2.0 @@ -280,7 +213,7 @@ def update_path(self, emitter=None): self.add_coord(xdestination, ydestination) -class SubprocessView(Subprocess, gui.SvgSubcontainer, MoveableWidget): +class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): label = None label_font_size = 12 @@ -291,7 +224,7 @@ class SubprocessView(Subprocess, gui.SvgSubcontainer, MoveableWidget): io_left_right_offset = 10 def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): - Subprocess.__init__(self, name) + FBD_model.FunctionBlock.__init__(self, name) gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) MoveableWidget.__init__(self, container, *args, **kwargs) @@ -306,7 +239,7 @@ def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): self.label.css_font_size = gui.to_pix(self.label_font_size) self.append(self.label) - #for all the outputs defined by decorator on Subprocess.do + #for all the outputs defined by decorator on FunctionBlock.do # add the related Outputs for o in self.do._outputs: self.add_io_widget(OutputView(o)) @@ -337,7 +270,7 @@ def add_io_widget(self, widget): widget.label.css_font_size = gui.to_pix(self.io_font_size) widget.set_size(len(widget.name) * self.io_font_size, self.io_font_size) - Subprocess.add_io(self, widget) + FBD_model.FunctionBlock.add_io(self, widget) self.append(widget) widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) @@ -374,45 +307,13 @@ def set_name(self, name): self.adjust_geometry() -class Process(): - subprocesses = None - def __init__(self): - self.subprocesses = {} - - def add_subprocess(self, subprocess): - self.subprocesses[subprocess.name] = subprocess - - def do(self): - for subprocesses in self.subprocesses.values(): - parameters = {} - all_inputs_connected = True - for IN in subprocesses.inputs.values(): - if IN.source == None: - all_inputs_connected = False - continue - parameters[IN.name] = IN.get_value() - - if not all_inputs_connected: - continue - output_results = subprocesses.do(**parameters) - if output_results is None: - continue - i = 0 - for OUT in subprocesses.outputs.values(): - if type(output_results) in (tuple, list): - OUT.set_value(output_results[i]) - else: - OUT.set_value(output_results) - i += 1 - - -class ProcessView(gui.Svg, Process): +class ProcessView(gui.Svg, FBD_model.Process): selected_input = None selected_output = None def __init__(self, *args, **kwargs): gui.Svg.__init__(self, *args, **kwargs) - Process.__init__(self) + FBD_model.Process.__init__(self) self.css_border_color = 'black' self.css_border_width = '1' self.css_border_style = 'solid' @@ -436,146 +337,162 @@ def onselection_end(self, emitter, x, y): if self.selected_input != None and self.selected_output != None: if self.selected_input.is_linked(): return - link = Link(self.selected_output, self.selected_input) + link = LinkView(self.selected_output, self.selected_input) self.append(link) - def add_subprocess(self, subprocess): - subprocess.onclick.do(self.onsubprocess_clicked) - self.append(subprocess) - Process.add_subprocess(self, subprocess) + def add_function_block(self, function_block): + function_block.onclick.do(self.onfunction_block_clicked) + self.append(function_block, function_block.name) + FBD_model.Process.add_function_block(self, function_block) @gui.decorate_event - def onsubprocess_clicked(self, subprocess): - return (subprocess,) + def onfunction_block_clicked(self, function_block): + return (function_block,) + + +class FBToolbox(gui.Container): + def __init__(self, appInstance, **kwargs): + self.appInstance = appInstance + super(FBToolbox, self).__init__(**kwargs) + self.lblTitle = gui.Label("Widgets Toolbox", height=20) + self.lblTitle.add_class("DialogTitle") + self.widgetsContainer = gui.HBox(width='100%', height='calc(100% - 20px)') + self.widgetsContainer.style.update({'overflow-y': 'scroll', + 'overflow-x': 'hidden', + 'align-items': 'flex-start', + 'flex-wrap': 'wrap', + 'background-color': 'white'}) + + self.append([self.lblTitle, self.widgetsContainer]) + + import FBD_library + # load all widgets + self.add_widget_to_collection(FBD_library.BOOL) + self.add_widget_to_collection(FBD_library.NOT) + self.add_widget_to_collection(FBD_library.AND) + self.add_widget_to_collection(FBD_library.OR) + self.add_widget_to_collection(FBD_library.XOR) + self.add_widget_to_collection(FBD_library.PULSAR) + self.add_widget_to_collection(FBD_library.STRING) + self.add_widget_to_collection(FBD_library.STRING_SWAP_CASE) + self.add_widget_to_collection(FBD_library.RISING_EDGE) + + def add_widget_to_collection(self, functionBlockClass, group='standard_tools', **kwargs_to_widget): + # create an helper that will be created on click + # the helper have to search for function that have 'return' annotation 'event_listener_setter' + if group not in self.widgetsContainer.children.keys(): + self.widgetsContainer.append(EditorAttributesGroup(group), group) + self.widgetsContainer.children[group].style['width'] = "100%" + + helper = FBHelper( + self.appInstance, functionBlockClass, **kwargs_to_widget) + helper.attributes['title'] = functionBlockClass.__doc__ + #self.widgetsContainer.append( helper ) + self.widgetsContainer.children[group].append(helper) + + +class FBHelper(gui.HBox): + """ Allocates the Widget to which it refers, + interfacing to the user in order to obtain the necessary attribute values + obtains the constructor parameters, asks for them in a dialog + puts the values in an attribute called constructor + """ + + def __init__(self, appInstance, functionBlockClass, **kwargs_to_widget): + self.kwargs_to_widget = kwargs_to_widget + self.appInstance = appInstance + self.functionBlockClass = functionBlockClass + super(FBHelper, self).__init__() + self.style.update({'background-color': 'rgb(250,250,250)', 'width': "auto", 'margin':"2px", + "height": "60px", "justify-content": "center", "align-items": "center", + 'font-size': '12px'}) + if hasattr(functionBlockClass, "icon"): + if type(functionBlockClass.icon) == gui.Svg: + self.icon = functionBlockClass.icon + elif functionBlockClass.icon == None: + self.icon = default_icon(self.functionBlockClass.__name__) + else: + icon_file = functionBlockClass.icon + self.icon = gui.Image(icon_file, width='auto', margin='2px') + else: + icon_file = '/editor_resources:widget_%s.png' % self.functionBlockClass.__name__ + if os.path.exists(self.appInstance._get_static_file(icon_file)): + self.icon = gui.Image(icon_file, width='auto', margin='2px') + else: + self.icon = default_icon(self.functionBlockClass.__name__) + self.icon.style['max-width'] = '100%' + self.icon.style['image-rendering'] = 'auto' + self.icon.attributes['draggable'] = 'false' + self.icon.attributes['ondragstart'] = "event.preventDefault();" + self.append(self.icon, 'icon') + self.append(gui.Label(self.functionBlockClass.__name__), 'label') + self.children['label'].style.update({'margin-left':'2px', 'margin-right':'3px'}) -class STRING(SubprocessView): - @property - @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', str, {}) - def value(self): - if len(self.outputs) < 1: - return "" - return self.outputs['OUT'].get_value() - @value.setter - def value(self, value): self.outputs['OUT'].set_value(value) + self.attributes.update({'draggable': 'true', + 'ondragstart': "this.style.cursor='move'; event.dataTransfer.dropEffect = 'move'; event.dataTransfer.setData('application/json', JSON.stringify(['add',event.target.id,(event.clientX),(event.clientY)]));", + 'ondragover': "event.preventDefault();", + 'ondrop': "event.preventDefault();return false;"}) - def __init__(self, name, *args, **kwargs): - SubprocessView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value("A STRING VALUE") + # this dictionary will contain optional style attributes that have to be added to the widget once created + self.optional_style_dict = {} - @Subprocess.decorate_process(['OUT']) - def do(self): - OUT = self.outputs['OUT'].get_value() - return OUT + self.onclick.do(self.create_instance) -class STRING_SWAP_CASE(SubprocessView): - def __init__(self, name, *args, **kwargs): - SubprocessView.__init__(self, name, *args, **kwargs) - - @Subprocess.decorate_process(['OUT']) - def do(self, EN, IN): - if not EN: + def build_widget_name_list_from_tree(self, node): + if not issubclass(type(node), FBD_model.FunctionBlock) and not issubclass(type(node), ProcessView): return - OUT = IN.swapcase() - return OUT - -class BOOL(SubprocessView): - @property - @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', bool, {}) - def value(self): - if len(self.outputs) < 1: - return False - return self.outputs['OUT'].get_value() - @value.setter - def value(self, value): self.outputs['OUT'].set_value(value) - - def __init__(self, name, *args, **kwargs): - SubprocessView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value(False) - - @Subprocess.decorate_process(['OUT']) - def do(self): - OUT = self.outputs['OUT'].get_value() - return OUT - -class RISING_EDGE(SubprocessView): - previous_value = None - - @Subprocess.decorate_process(['OUT']) - def do(self, IN): - OUT = (self.previous_value != IN) and IN - self.previous_value = IN - return OUT - -class NOT(SubprocessView): - @Subprocess.decorate_process(['OUT']) - def do(self, IN): - OUT = not IN - return OUT - -class AND(SubprocessView): - @Subprocess.decorate_process(['OUT']) - def do(self, IN1, IN2): - OUT = IN1 and IN2 - return OUT - -class OR(SubprocessView): - @Subprocess.decorate_process(['OUT']) - def do(self, IN1, IN2): - OUT = IN1 or IN2 - return OUT - -class XOR(SubprocessView): - @Subprocess.decorate_process(['OUT']) - def do(self, IN1, IN2): - OUT = IN1 != IN2 - return OUT - -class PULSAR(SubprocessView): - ton = 1000 - toff = 1000 - tstart = 0 - def __init__(self, name, *args, **kwargs): - SubprocessView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value(False) - self.tstart = time.time() - - @Subprocess.decorate_process(['OUT']) - def do(self): - OUT = (int((time.time() - self.tstart)*1000) % (self.ton + self.toff)) < self.ton - return OUT - + if issubclass(type(node), FBD_model.FunctionBlock): + self.varname_list.append(node.name) + for child in node.children.values(): + self.build_widget_name_list_from_tree(child) -class Toolbox(gui.VBox): - process_view = None + def build_widget_used_keys_list_from_tree(self, node): + if not issubclass(type(node), FBD_model.FunctionBlock) and not issubclass(type(node), ProcessView): + return + self.used_keys_list.extend(list(node.children.keys())) + for child in node.children.values(): + self.build_widget_used_keys_list_from_tree(child) - index_added_tool = 0 + def on_dropped(self, left, top): + self.optional_style_dict['left'] = gui.to_pix(left) + self.optional_style_dict['top'] = gui.to_pix(top) + self.create_instance(None) - def __init__(self, process_view, *args, **kwargs): - gui.VBox.__init__(self, *args, **kwargs) - self.process_view = process_view - self.append(gui.Label("Toolbox", width="100%", height="auto", style={'outline':'1px solid black'})) + def create_instance(self, widget): + """ Here the widget is allocated + """ + self.varname_list = [] + self.build_widget_name_list_from_tree(self.appInstance.process) + self.used_keys_list = [] + self.build_widget_used_keys_list_from_tree(self.appInstance.process) + print("-------------used keys:" + str(self.used_keys_list)) + variableName = '' + for i in range(0, 1000): # reasonably no more than 1000 widget instances in a project + variableName = self.functionBlockClass.__name__.lower() + str(i) + if not variableName in self.varname_list and not variableName in self.used_keys_list: + break - self.container = gui.VBox(width="100%", height="auto", style={'outline':'1px solid black'}) - self.container.css_justify_content = 'flex-start' - self.container.style['row-gap'] = '10px' - self.append(self.container) + """ + if re.match('(^[a-zA-Z][a-zA-Z0-9_]*)|(^[_][a-zA-Z0-9_]+)', variableName) == None: + self.errorDialog = gui.GenericDialog("Error", "Please type a valid variable name.", width=350,height=120) + self.errorDialog.show(self.appInstance) + return - self.css_justify_content = 'flex-start' + if variableName in self.varname_list: + self.errorDialog = gui.GenericDialog("Error", "The typed variable name is already used. Please specify a new name.", width=350,height=150) + self.errorDialog.show(self.appInstance) + return + """ + # here we create and decorate the widget + function_block = self.functionBlockClass(variableName, self.appInstance.process, **self.kwargs_to_widget) + function_block.attr_editor_newclass = False - def add_tool(self, tool_class): - tool_class_widget = gui.Label(tool_class.__name__, style={'outline':'1px solid black'}) - tool_class_widget.tool_class = tool_class - tool_class_widget.onclick.do(self.on_tool_selected) - tool_class_widget.style['cursor'] = 'pointer' - self.container.append(tool_class_widget) + for key in self.optional_style_dict: + function_block.style[key] = self.optional_style_dict[key] + self.optional_style_dict = {} - def on_tool_selected(self, tool_class_widget): - tool_class = tool_class_widget.tool_class - tool_instance = tool_class(tool_class.__name__ , self.process_view) - tool_instance.set_name(tool_instance.name + "_" + str(self.index_added_tool)) - self.index_added_tool += 1 - self.process_view.add_subprocess(tool_instance) + self.appInstance.add_function_block_to_editor(function_block) class MyApp(App): @@ -602,50 +519,22 @@ def main(self): ) self.process = ProcessView(width=600, height=600) - self.process.onsubprocess_clicked.do(self.onprocessview_subprocess_clicked) + self.process.onfunction_block_clicked.do(self.onprocessview_function_block_clicked) self.attributes_editor = EditorAttributes(self) - self.toolbox = Toolbox(self.process) - self.toolbox.add_tool(BOOL) - self.toolbox.add_tool(NOT) - self.toolbox.add_tool(AND) - self.toolbox.add_tool(OR) - self.toolbox.add_tool(XOR) - self.toolbox.add_tool(PULSAR) - self.toolbox.add_tool(STRING) - self.toolbox.add_tool(STRING_SWAP_CASE) - self.toolbox.add_tool(RISING_EDGE) + self.toolbox = FBToolbox(self) self.main_container.append(self.toolbox, 'toolbox') self.main_container.append(self.process, 'process_view') self.main_container.append(self.attributes_editor, 'attributes') - """ - y = 10 - m = BOOL("BOOL", False, self.process, 100, y) - self.process.add_subprocess(m) - - y += 110 - m = BOOL("BOOL 2", True, self.process, 100, y) - self.process.add_subprocess(m) - - y += 110 - m = NOT("NOT 0", self.process, 100, y) - self.process.add_subprocess(m) - - y += 110 - m = AND("AND", self.process, 100, y) - self.process.add_subprocess(m) - - y += 110 - m = OR("OR", self.process, 100, y) - self.process.add_subprocess(m) - """ - # returning the root widget return self.main_container - def onprocessview_subprocess_clicked(self, emitter, subprocess): - self.attributes_editor.set_widget(subprocess) + def onprocessview_function_block_clicked(self, emitter, function_block): + self.attributes_editor.set_widget(function_block) + + def add_function_block_to_editor(self, function_block): + self.process.add_function_block(function_block) if __name__ == "__main__": From d0fcf873b96fe4e621ceeeb017d9c3048bdd49c3 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 22 Nov 2022 09:13:11 +0100 Subject: [PATCH 086/110] BugFix Link now works again. --- editor/FBD_model.py | 3 --- editor/FBD_view.py | 42 +++++++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/editor/FBD_model.py b/editor/FBD_model.py index 12f57848..2455db7c 100644 --- a/editor/FBD_model.py +++ b/editor/FBD_model.py @@ -85,13 +85,10 @@ class Link(): destination = None def __init__(self, source_widget, destination_widget): self.source = source_widget - self.source.onpositionchanged.do(self.update_path) self.destination = destination_widget - self.destination.onpositionchanged.do(self.update_path) self.source.destinaton = self.destination self.destination.source = self.source - self.update_path() def unlink(self): self.get_parent().remove_child(self.unlink_bt) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index a188f5a1..07a47667 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -133,29 +133,37 @@ def onpositionchanged(self): return () +class Unlink(gui.SvgSubcontainer): + def __init__(self, x=0, y=0, w=10, h=10, *args, **kwargs): + gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) + line = gui.SvgLine(0,0,"100%","100%") + line.set_stroke(2, 'red') + self.append(line) + line = gui.SvgLine("100%", 0, 0, "100%") + line.set_stroke(2, 'red') + self.append(line) + + class LinkView(gui.SvgPolyline, FBD_model.Link): + bt_unlink = None def __init__(self, source_widget, destination_widget, *args, **kwargs): gui.SvgPolyline.__init__(self, 2, *args, **kwargs) - FBD_model.Link.__init__(source_widget, destination_widget) + FBD_model.Link.__init__(self, source_widget, destination_widget) self.set_stroke(1, 'black') self.set_fill('transparent') self.attributes['stroke-dasharray'] = "4 2" + self.source.onpositionchanged.do(self.update_path) + self.destination.onpositionchanged.do(self.update_path) self.update_path() - self.unlink_bt = gui.SvgSubcontainer(0,0,0,0) - line = gui.SvgLine(0,0,"100%","100%") - line.set_stroke(1, 'red') - self.unlink_bt.append(line) - line = gui.SvgLine("100%", 0, 0, "100%") - line.set_stroke(1, 'red') - self.unlink_bt.append(line) - self.get_parent().append(self.unlink_bt) - self.unlink_bt.onclick.do(self.unlink) + def set_unlink_button(self, bt_unlink): + self.bt_unlink = bt_unlink + self.bt_unlink.onclick.do(self.unlink) def unlink(self, emitter): - self.get_parent().remove_child(self.unlink_bt) + self.get_parent().remove_child(self.bt_unlink) self.get_parent().remove_child(self) - FBD_model.Link.unlink() + FBD_model.Link.unlink(self) def update_path(self, emitter=None): self.attributes['points'] = '' @@ -211,7 +219,8 @@ def update_path(self, emitter=None): self.add_coord(xdestination - (xdestination-xsource)/2.0, ydestination) self.add_coord(xdestination, ydestination) - + if self.bt_unlink != None: + self.bt_unlink.set_position(xdestination - offset / 2.0, ydestination) class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): @@ -322,14 +331,14 @@ def __init__(self, *args, **kwargs): def onselection_start(self, emitter, x, y): self.selected_input = self.selected_output = None print("selection start: ", type(emitter)) - if type(emitter) == InputView: + if issubclass(type(emitter), FBD_model.Input): self.selected_input = emitter else: self.selected_output = emitter def onselection_end(self, emitter, x, y): print("selection end: ", type(emitter)) - if type(emitter) == InputView: + if issubclass(type(emitter), FBD_model.Input): self.selected_input = emitter else: self.selected_output = emitter @@ -339,6 +348,9 @@ def onselection_end(self, emitter, x, y): return link = LinkView(self.selected_output, self.selected_input) self.append(link) + bt_unlink = Unlink() + self.append(bt_unlink) + link.set_unlink_button(bt_unlink) def add_function_block(self, function_block): function_block.onclick.do(self.onfunction_block_clicked) From 69d34dcf90ab80a8bb6b8733643a5fed8c9b5054 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 22 Nov 2022 09:40:30 +0100 Subject: [PATCH 087/110] FBD link-unlink improved. --- editor/FBD_library.py | 21 ++++++++++++++++++--- editor/FBD_model.py | 4 +++- editor/FBD_view.py | 15 ++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/editor/FBD_library.py b/editor/FBD_library.py index ad522145..cedd0048 100644 --- a/editor/FBD_library.py +++ b/editor/FBD_library.py @@ -16,7 +16,7 @@ def value(self): def value(self, value): self.outputs['OUT'].set_value(value) def __init__(self, name, *args, **kwargs): - FBD_view.FunctionBlockViewFunctionBlockView.__init__(self, name, *args, **kwargs) + FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) self.outputs['OUT'].set_value("A STRING VALUE") @FBD_model.FunctionBlock.decorate_process(['OUT']) @@ -88,8 +88,23 @@ def do(self, IN1, IN2): return OUT class PULSAR(FBD_view.FunctionBlockView): - ton = 1000 - toff = 1000 + _ton = 1000 + _toff = 1000 + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual TON value''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + def ton(self): + return self._ton + @ton.setter + def ton(self, value): self._ton = value + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual TOFF value''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + def toff(self): + return self._toff + @toff.setter + def toff(self, value): self._toff = value + tstart = 0 def __init__(self, name, *args, **kwargs): FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) diff --git a/editor/FBD_model.py b/editor/FBD_model.py index 2455db7c..a9b2d85a 100644 --- a/editor/FBD_model.py +++ b/editor/FBD_model.py @@ -91,8 +91,10 @@ def __init__(self, source_widget, destination_widget): self.destination.source = self.source def unlink(self): - self.get_parent().remove_child(self.unlink_bt) + self.get_parent().remove_child(self.bt_unlink) self.get_parent().remove_child(self) + self.source.unlink() + self.destination.unlink() class Process(): diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 07a47667..41e6641a 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -143,6 +143,11 @@ def __init__(self, x=0, y=0, w=10, h=10, *args, **kwargs): line.set_stroke(2, 'red') self.append(line) + def get_size(self): + """ Returns the rectangle size. + """ + return float(self.attr_width), float(self.attr_height) + class LinkView(gui.SvgPolyline, FBD_model.Link): bt_unlink = None @@ -159,6 +164,7 @@ def __init__(self, source_widget, destination_widget, *args, **kwargs): def set_unlink_button(self, bt_unlink): self.bt_unlink = bt_unlink self.bt_unlink.onclick.do(self.unlink) + self.update_path() def unlink(self, emitter): self.get_parent().remove_child(self.bt_unlink) @@ -220,7 +226,10 @@ def update_path(self, emitter=None): self.add_coord(xdestination, ydestination) if self.bt_unlink != None: - self.bt_unlink.set_position(xdestination - offset / 2.0, ydestination) + + w, h = self.bt_unlink.get_size() + self.bt_unlink.set_position(xdestination - offset / 2.0 - w/2, ydestination -h/2) + class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): @@ -257,6 +266,8 @@ def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): for arg in signature.parameters: self.add_io_widget(InputView(arg)) + self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) + def calc_height(self): inputs_count = 0 if self.inputs == None else len(self.inputs) outputs_count = 0 if self.outputs == None else len(self.outputs) @@ -293,12 +304,14 @@ def adjust_geometry(self): i = 1 for inp in self.inputs.values(): inp.set_position(0, self.label_font_size + self.io_font_size*i) + inp.onpositionchanged() i += 1 i = 1 for o in self.outputs.values(): ow, oh = o.get_size() o.set_position(w - ow, self.label_font_size + self.io_font_size*i) + o.onpositionchanged() i += 1 def set_position(self, x, y): From f7144f1098f5728ff98d7138290f82feba6b78d3 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 22 Nov 2022 16:06:51 +0100 Subject: [PATCH 088/110] FBD ObjectBlock. --- editor/FBD_library.py | 13 ++- editor/FBD_model.py | 38 ++++++-- editor/FBD_view.py | 204 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 231 insertions(+), 24 deletions(-) diff --git a/editor/FBD_library.py b/editor/FBD_library.py index cedd0048..d3db464b 100644 --- a/editor/FBD_library.py +++ b/editor/FBD_library.py @@ -4,6 +4,16 @@ import remi import remi.gui as gui import time +import inspect +import types + + +class PRINT(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process([]) + def do(self, IN, EN = True): + if not EN: + return + print(IN) class STRING(FBD_view.FunctionBlockView): @property @@ -25,9 +35,6 @@ def do(self): return OUT class STRING_SWAP_CASE(FBD_view.FunctionBlockView): - def __init__(self, name, *args, **kwargs): - FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) - @FBD_model.FunctionBlock.decorate_process(['OUT']) def do(self, IN, EN = True): if not EN: diff --git a/editor/FBD_model.py b/editor/FBD_model.py index a9b2d85a..a5b36d6d 100644 --- a/editor/FBD_model.py +++ b/editor/FBD_model.py @@ -2,16 +2,23 @@ class Input(): name = None + default = None typ = None source = None #has to be an Output - def __init__(self, name, typ = None): + def __init__(self, name, default = inspect.Parameter.empty, typ = None): self.name = name + self.default = default self.typ = typ def get_value(self): + if not self.is_linked(): + return self.default return self.source.get_value() + def has_default(self): + return not (self.default == inspect.Parameter.empty) + def link(self, output): self.source = output @@ -48,6 +55,15 @@ def unlink(self): self.link(None) +class ObjectBlock(): + name = None + FBs = None #this is the list of member functions + + def __init__(self, name): + self.name = name + self.FBs = {} + + class FunctionBlock(): name = None inputs = None @@ -99,24 +115,33 @@ def unlink(self): class Process(): function_blocks = None + object_blocks = None + def __init__(self): self.function_blocks = {} + self.object_blocks = {} def add_function_block(self, function_block): self.function_blocks[function_block.name] = function_block + def add_object_block(self, object_block): + self.object_blocks[object_block.name] = object_block + def do(self): - for function_block in self.function_blocks.values(): + sub_function_blocks = [] + for object_block in self.object_blocks.values(): + for function_block in object_block.FBs.values(): + sub_function_blocks.append(function_block) + + for function_block in (*self.function_blocks.values(), *sub_function_blocks): parameters = {} all_inputs_connected = True - function_block_default_inputs = inspect.signature(function_block.do).parameters - for IN in function_block.inputs.values(): - if (not IN.is_linked()) and function_block_default_inputs[IN.name].default == inspect.Parameter.empty: + if (not IN.is_linked()) and (not IN.has_default()): all_inputs_connected = False continue - parameters[IN.name] = IN.get_value() if IN.is_linked() else function_block_default_inputs[IN.name].default + parameters[IN.name] = IN.get_value() if not all_inputs_connected: continue @@ -130,3 +155,4 @@ def do(self): else: OUT.set_value(output_results) i += 1 + diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 41e6641a..22fd349c 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -20,6 +20,7 @@ import time from editor_widgets import * import FBD_model +import types class MixinPositionSize(): def get_position(self): @@ -79,7 +80,7 @@ def __init__(self, name, *args, **kwargs): self.label.style['cursor'] = 'pointer' self.append(self.label) - FBD_model.Input.__init__(self, name, "") + FBD_model.Input.__init__(self, name, *args, **kwargs) def set_size(self, width, height): if self.placeholder: @@ -108,7 +109,7 @@ def __init__(self, name, *args, **kwargs): self.label.style['cursor'] = 'pointer' self.append(self.label) - FBD_model.Output.__init__(self, name, "") + FBD_model.Output.__init__(self, name, *args, **kwargs) def set_size(self, width, height): if self.placeholder: @@ -151,7 +152,9 @@ def get_size(self): class LinkView(gui.SvgPolyline, FBD_model.Link): bt_unlink = None - def __init__(self, source_widget, destination_widget, *args, **kwargs): + container = None + def __init__(self, source_widget, destination_widget, container, *args, **kwargs): + self.container = container gui.SvgPolyline.__init__(self, 2, *args, **kwargs) FBD_model.Link.__init__(self, source_widget, destination_widget) self.set_stroke(1, 'black') @@ -171,25 +174,37 @@ def unlink(self, emitter): self.get_parent().remove_child(self) FBD_model.Link.unlink(self) + def get_absolute_node_position(self, node): + np = node.get_parent() + if np == self.container: + return node.get_position() + + x, y = node.get_position() + xs, ys = self.get_absolute_node_position(np) + return x+xs, y+ys + def update_path(self, emitter=None): self.attributes['points'] = '' - x,y = self.source.get_position() + xsource,ysource = self.get_absolute_node_position( self.source ) w,h = self.source.get_size() - xsource_parent, ysource_parent = self.source.get_parent().get_position() + xsource += w + ysource += h/2 + xsource_parent, ysource_parent = self.get_absolute_node_position(self.source.get_parent()) wsource_parent, hsource_parent = self.source.get_parent().get_size() - xsource = xsource_parent + wsource_parent - ysource = ysource_parent + y + h/2.0 + #xsource = xsource_parent + wsource_parent + #ysource = ysource_parent + y + h/2.0 self.add_coord(xsource, ysource) - x,y = self.destination.get_position() + x,y = self.get_absolute_node_position( self.destination ) w,h = self.destination.get_size() - xdestination_parent, ydestination_parent = self.destination.get_parent().get_position() + xdestination_parent, ydestination_parent = self.get_absolute_node_position(self.destination.get_parent()) wdestination_parent, hdestination_parent = self.destination.get_parent().get_size() - xdestination = xdestination_parent - ydestination = ydestination_parent + y + h/2.0 + xdestination, ydestination = self.get_absolute_node_position( self.destination ) + ydestination += + h/2.0 + #ydestination = ydestination_parent + y + h/2.0 offset = 10 @@ -231,6 +246,95 @@ def update_path(self, emitter=None): self.bt_unlink.set_position(xdestination - offset / 2.0 - w/2, ydestination -h/2) +class ObjectBlockView(FBD_model.ObjectBlock, gui.SvgSubcontainer, MoveableWidget): + + label = None + label_font_size = 12 + + outline = None + + reference_object = None + + def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): + name = obj.__class__.__name__ + self.reference_object = obj + FBD_model.ObjectBlock.__init__(self, name) + gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) + MoveableWidget.__init__(self, container, *args, **kwargs) + + self.outline = gui.SvgRectangle(0, 0, "100%", "100%") + self.outline.set_fill('white') + self.outline.set_stroke(2, 'orange') + self.append(self.outline) + + self.label = gui.SvgText("50%", 0, self.name) + self.label.attr_text_anchor = "middle" + self.label.attr_dominant_baseline = 'hanging' + self.label.css_font_size = gui.to_pix(self.label_font_size) + self.append(self.label) + + self.onselection_start = self.container.onselection_start + self.onselection_end = self.container.onselection_end + + for (method_name, method) in inspect.getmembers(self.reference_object, inspect.ismethod): + #try: + #c = types.new_class(method_name, (FunctionBlockView,)) + #setattr(c, "do", types.MethodType(getattr(self.reference_object, method_name), c)) + #c.do.__dict__['_outputs'] = [] + #FBD_model.FunctionBlock.decorate_process(['OUT'])(c.do) + #self.add_fb_view(c(method_name, container)) + self.add_fb_view(ObjectFunctionBlockView(self.reference_object, method, method_name, method_name, self)) + #except: + # pass + + self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) + + def calc_height(self): + xmax = ymax = 0 + if not self.FBs is None: + for fb in self.FBs.values(): + x, y = fb.get_position() + w, h = fb.get_size() + xmax = max(xmax, x+w) + ymax = max(ymax, y+h) + + return self.label_font_size + ymax + + def calc_width(self): + xmax = ymax = 0 + if not self.FBs is None: + for fb in self.FBs.values(): + x, y = fb.get_position() + w, h = fb.get_size() + xmax = max(xmax, x+w) + ymax = max(ymax, y+h) + + return max((len(self.name) * self.label_font_size), xmax) + + def add_fb_view(self, fb_view_instance): + self.FBs[fb_view_instance.name] = fb_view_instance + + self.append(fb_view_instance) + for fb in self.FBs.values(): + fb.adjust_geometry() + self.adjust_geometry() + + def adjust_geometry(self): + gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) + + def set_position(self, x, y): + for fb in self.FBs.values(): + fb.adjust_geometry() + #w, h = self.get_size() + #self.attr_viewBox = "%s %s %s %s"%(x, y, x+w, y+h) + return super().set_position(x, y) + + def set_name(self, name): + self.name = name + self.label.set_text(name) + self.adjust_geometry() + + class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): label = None @@ -259,12 +363,13 @@ def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): #for all the outputs defined by decorator on FunctionBlock.do # add the related Outputs - for o in self.do._outputs: - self.add_io_widget(OutputView(o)) + if hasattr(self.do, "_outputs"): + for o in self.do._outputs: + self.add_io_widget(OutputView(o)) signature = inspect.signature(self.do) for arg in signature.parameters: - self.add_io_widget(InputView(arg)) + self.add_io_widget(InputView(arg, default = signature.parameters[arg].default)) self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) @@ -329,6 +434,67 @@ def set_name(self, name): self.adjust_geometry() +class ObjectFunctionBlockView(FunctionBlockView): + reference_object = None + method = None + method_name = None + + processed_outputs = False + + def __init__(self, obj, method, method_name, name, container, x = 10, y = 10, *args, **kwargs): + self.reference_object = obj + self.method = method + self.method_name = method_name + FBD_model.FunctionBlock.__init__(self, name) + gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) + MoveableWidget.__init__(self, container, *args, **kwargs) + + self.outline = gui.SvgRectangle(0, 0, "100%", "100%") + self.outline.set_fill('white') + self.outline.set_stroke(2, 'black') + self.append(self.outline) + + self.label = gui.SvgText("50%", 0, self.name) + self.label.attr_text_anchor = "middle" + self.label.attr_dominant_baseline = 'hanging' + self.label.css_font_size = gui.to_pix(self.label_font_size) + self.append(self.label) + + #for all the outputs defined by decorator on FunctionBlock.do + # add the related Outputs + #if hasattr(self.do, "_outputs"): + # for o in self.do._outputs: + # self.add_io_widget(OutputView(o)) + + signature = inspect.signature(getattr(self.reference_object, self.method_name)) + for arg in signature.parameters: + self.add_io_widget(InputView(arg, default=signature.parameters[arg].default)) + self.add_io_widget(InputView('EN', default=False)) + + #self.do = getattr(self.reference_object, self.method_name) + + self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) + + def do(self, *args, **kwargs): + if kwargs.get('EN') != None: + if kwargs['EN'] == False: + return + del kwargs['EN'] + + output = getattr(self.reference_object, self.method_name)(*args, **kwargs) + if self.processed_outputs == False: + if not output is None: + self.add_io_widget(OutputView('OUT' + str(0))) + if type(output) in (tuple,): + if len(output) > 1: + i = 1 + for o in output: + self.add_io_widget(OutputView('OUT' + str(i))) + i += 1 + self.processed_outputs = True + return output + + class ProcessView(gui.Svg, FBD_model.Process): selected_input = None selected_output = None @@ -359,7 +525,7 @@ def onselection_end(self, emitter, x, y): if self.selected_input != None and self.selected_output != None: if self.selected_input.is_linked(): return - link = LinkView(self.selected_output, self.selected_input) + link = LinkView(self.selected_output, self.selected_input, self) self.append(link) bt_unlink = Unlink() self.append(bt_unlink) @@ -370,6 +536,11 @@ def add_function_block(self, function_block): self.append(function_block, function_block.name) FBD_model.Process.add_function_block(self, function_block) + def add_object_block(self, object_block): + object_block.onclick.do(self.onfunction_block_clicked) + self.append(object_block, object_block.name) + FBD_model.Process.add_object_block(self, object_block) + @gui.decorate_event def onfunction_block_clicked(self, function_block): return (function_block,) @@ -401,6 +572,7 @@ def __init__(self, appInstance, **kwargs): self.add_widget_to_collection(FBD_library.STRING) self.add_widget_to_collection(FBD_library.STRING_SWAP_CASE) self.add_widget_to_collection(FBD_library.RISING_EDGE) + self.add_widget_to_collection(FBD_library.PRINT) def add_widget_to_collection(self, functionBlockClass, group='standard_tools', **kwargs_to_widget): # create an helper that will be created on click @@ -547,6 +719,8 @@ def main(self): self.process.onfunction_block_clicked.do(self.onprocessview_function_block_clicked) self.attributes_editor = EditorAttributes(self) self.toolbox = FBToolbox(self) + + self.process.add_object_block(ObjectBlockView(gui.TextInput(), self.process)) self.main_container.append(self.toolbox, 'toolbox') self.main_container.append(self.process, 'process_view') From ec95d0c1562cb8d94a777427a759bb45355813bb Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 22 Nov 2022 16:49:49 +0100 Subject: [PATCH 089/110] FBD ObjectBlock methods are called correctly, and ObjectBlock mouse drag works. --- editor/FBD_view.py | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 22fd349c..292a55f3 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -41,6 +41,7 @@ def __init__(self, container, *args, **kwargs): def start_drag(self, emitter, x, y): self.active = True self.container.onmousemove.do(self.on_drag, js_stop_propagation=True, js_prevent_default=True) + self.onmousemove.do(None, js_stop_propagation=False, js_prevent_default=True) self.container.onmouseup.do(self.stop_drag) self.container.onmouseleave.do(self.stop_drag, 0, 0) @@ -66,21 +67,46 @@ def __init__(self, text='svg text', *args, **kwargs): class InputView(FBD_model.Input, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None + previous_value = None + def __init__(self, name, *args, **kwargs): gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) self.placeholder = gui.SvgRectangle(0, 0, 0, 0) + self.append(self.placeholder) + + self.label = gui.SvgText("0%", "50%", name) + self.append(self.label) + + FBD_model.Input.__init__(self, name, *args, **kwargs) + self.set_default_look() + + def set_default_look(self): self.placeholder.set_stroke(1, 'black') self.placeholder.set_fill("lightgray") self.placeholder.style['cursor'] = 'pointer' - self.append(self.placeholder) - self.label = gui.SvgText("0%", "50%", name) self.label.attr_dominant_baseline = 'middle' self.label.attr_text_anchor = "start" self.label.style['cursor'] = 'pointer' - self.append(self.label) + self.label.set_fill('black') - FBD_model.Input.__init__(self, name, *args, **kwargs) + def unlink(self): + ret = super().unlink() + self.set_default_look() + return ret + + def get_value(self): + v = FBD_model.Input.get_value(self) + + if self.is_linked() or self.has_default(): + if self.previous_value != v: + if type(v) == bool: + self.label.set_fill('white') + self.placeholder.set_fill('blue' if v else 'BLACK') + self.append(SvgTitle(str(v)), "title") + self.previous_value = v + + return v def set_size(self, width, height): if self.placeholder: @@ -317,9 +343,12 @@ def add_fb_view(self, fb_view_instance): self.append(fb_view_instance) for fb in self.FBs.values(): fb.adjust_geometry() + fb.on_drag.do(lambda emitter, x, y:self.adjust_geometry()) self.adjust_geometry() def adjust_geometry(self): + for fb in self.FBs.values(): + fb.adjust_geometry() gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) def set_position(self, x, y): @@ -327,7 +356,7 @@ def set_position(self, x, y): fb.adjust_geometry() #w, h = self.get_size() #self.attr_viewBox = "%s %s %s %s"%(x, y, x+w, y+h) - return super().set_position(x, y) + return gui.SvgSubcontainer.set_position(self, x, y) def set_name(self, name): self.name = name @@ -491,6 +520,7 @@ def do(self, *args, **kwargs): for o in output: self.add_io_widget(OutputView('OUT' + str(i))) i += 1 + self.processed_outputs = True return output From 1b8e572c569a379fbaa794d075476907acc52b68 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 22 Nov 2022 17:16:43 +0100 Subject: [PATCH 090/110] FBD ObjectBlock mouse drag a little faster. --- editor/FBD_view.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 292a55f3..e918997f 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -343,17 +343,21 @@ def add_fb_view(self, fb_view_instance): self.append(fb_view_instance) for fb in self.FBs.values(): fb.adjust_geometry() - fb.on_drag.do(lambda emitter, x, y:self.adjust_geometry()) + fb.on_drag.do(self.onfunction_block_position_changed) + self.adjust_geometry() + + def onfunction_block_position_changed(self, emitter, x, y): + emitter.adjust_geometry() self.adjust_geometry() def adjust_geometry(self): - for fb in self.FBs.values(): - fb.adjust_geometry() + #for fb in self.FBs.values(): + # fb.adjust_geometry() gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) def set_position(self, x, y): for fb in self.FBs.values(): - fb.adjust_geometry() + fb.onposition_changed() #w, h = self.get_size() #self.attr_viewBox = "%s %s %s %s"%(x, y, x+w, y+h) return gui.SvgSubcontainer.set_position(self, x, y) @@ -431,6 +435,13 @@ def add_io_widget(self, widget): self.adjust_geometry() + def onposition_changed(self): + for inp in self.inputs.values(): + inp.onpositionchanged() + + for o in self.outputs.values(): + o.onpositionchanged() + def adjust_geometry(self): gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) w, h = self.get_size() From 728004acd19008885e3329e1b6baa5d0e5d02179 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Tue, 22 Nov 2022 17:58:44 +0100 Subject: [PATCH 091/110] FBD ObjectBlock gui.ClassEventConnectors. --- editor/FBD_view.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index e918997f..96971563 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -312,7 +312,13 @@ def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): self.add_fb_view(ObjectFunctionBlockView(self.reference_object, method, method_name, method_name, self)) #except: # pass - + + for (class_name, _class) in inspect.getmembers(self.reference_object): + evt = getattr(self.reference_object, class_name) + if issubclass(type(_class), gui.ClassEventConnector): + #self.append(ObjectBlockView(evt, self)) + self.add_fb_view(ObjectFunctionBlockView(evt, evt, "do", evt.event_method_bound.__name__ + ".do", self)) + self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) def calc_height(self): From 9abbec7020229b819ed39830ac2b592575c6a502 Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 23 Nov 2022 00:02:54 +0100 Subject: [PATCH 092/110] FBD Testing adapters for remi widgets. --- editor/FBD_view.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 96971563..35a93574 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -302,6 +302,7 @@ def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): self.onselection_start = self.container.onselection_start self.onselection_end = self.container.onselection_end + """ for (method_name, method) in inspect.getmembers(self.reference_object, inspect.ismethod): #try: #c = types.new_class(method_name, (FunctionBlockView,)) @@ -318,7 +319,7 @@ def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): if issubclass(type(_class), gui.ClassEventConnector): #self.append(ObjectBlockView(evt, self)) self.add_fb_view(ObjectFunctionBlockView(evt, evt, "do", evt.event_method_bound.__name__ + ".do", self)) - + """ self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) def calc_height(self): @@ -374,6 +375,19 @@ def set_name(self, name): self.adjust_geometry() +class TextInputAdapter(ObjectBlockView): + def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): + ObjectBlockView.__init__(self, obj, container, x = 10, y = 10, *args, **kwargs) + + txt = gui.TextInput() + ofbv = ObjectFunctionBlockView(self.reference_object, txt.get_value, "get_value", "get_value", self) + ofbv.add_io_widget(OutputView("Value")) + self.add_fb_view(ofbv) + + ofbv = ObjectFunctionBlockView(self.reference_object, txt.set_value, "set_value", "set_value", self) + self.add_fb_view(ofbv) + + class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): label = None @@ -485,8 +499,6 @@ class ObjectFunctionBlockView(FunctionBlockView): method = None method_name = None - processed_outputs = False - def __init__(self, obj, method, method_name, name, container, x = 10, y = 10, *args, **kwargs): self.reference_object = obj self.method = method @@ -528,6 +540,7 @@ def do(self, *args, **kwargs): del kwargs['EN'] output = getattr(self.reference_object, self.method_name)(*args, **kwargs) + """ #this is to populate outputs automatically if self.processed_outputs == False: if not output is None: self.add_io_widget(OutputView('OUT' + str(0))) @@ -539,6 +552,7 @@ def do(self, *args, **kwargs): i += 1 self.processed_outputs = True + """ return output @@ -767,7 +781,7 @@ def main(self): self.attributes_editor = EditorAttributes(self) self.toolbox = FBToolbox(self) - self.process.add_object_block(ObjectBlockView(gui.TextInput(), self.process)) + self.process.add_object_block(TextInputAdapter(gui.TextInput(), self.process)) self.main_container.append(self.toolbox, 'toolbox') self.main_container.append(self.process, 'process_view') From 016b919ce7d01a347300f10d9f5d250dba5049df Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 23 Nov 2022 11:36:32 +0100 Subject: [PATCH 093/110] FBD mouse interaction not affected anymore by moving over links. --- editor/FBD_view.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 35a93574..9682298c 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -161,8 +161,13 @@ def onpositionchanged(self): class Unlink(gui.SvgSubcontainer): - def __init__(self, x=0, y=0, w=10, h=10, *args, **kwargs): + def __init__(self, x=0, y=0, w=15, h=15, *args, **kwargs): gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) + self.outline = gui.SvgRectangle(0, 0, "100%", "100%") + self.outline.set_fill('white') + self.outline.set_stroke(1, 'black') + self.append(self.outline) + line = gui.SvgLine(0,0,"100%","100%") line.set_stroke(2, 'red') self.append(line) @@ -188,6 +193,12 @@ def __init__(self, source_widget, destination_widget, container, *args, **kwargs self.attributes['stroke-dasharray'] = "4 2" self.source.onpositionchanged.do(self.update_path) self.destination.onpositionchanged.do(self.update_path) + + #this is to prevent stopping elements drag when moving over a link + self.style['pointer-events'] = 'none' + self.onmousemove.do(None, js_stop_propagation=False, js_prevent_default=True) + self.onmouseleave.do(None, js_stop_propagation=False, js_prevent_default=True) + self.update_path() def set_unlink_button(self, bt_unlink): @@ -232,7 +243,7 @@ def update_path(self, emitter=None): ydestination += + h/2.0 #ydestination = ydestination_parent + y + h/2.0 - offset = 10 + offset = 20 if xdestination - xsource < offset*2: self.maxlen = 6 From 2a57a388bbeea55f5b23225a9a675e2dbb07dc5e Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 23 Nov 2022 12:26:03 +0100 Subject: [PATCH 094/110] FBD links improvements. --- editor/FBD_model.py | 22 ++++++++++++---------- editor/FBD_view.py | 32 ++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/editor/FBD_model.py b/editor/FBD_model.py index a5b36d6d..e9c7fc21 100644 --- a/editor/FBD_model.py +++ b/editor/FBD_model.py @@ -26,18 +26,19 @@ def is_linked(self): return self.source != None def unlink(self): - self.link(None) + Input.link(self, None) class Output(): name = None typ = None - destination = None #has to be an Input + destinations = None #has to be an Input value = None def __init__(self, name, typ = None): self.name = name self.typ = typ + self.destinations = [] def get_value(self): return self.value @@ -45,14 +46,17 @@ def get_value(self): def set_value(self, value): self.value = value - def link(self, input): - self.destination = input + def link(self, destination): + self.destinations.append(destination) def is_linked(self): - return self.destination != None + return len(self.destinations) > 0 - def unlink(self): - self.link(None) + def unlink(self, destination = None): + if not destination is None: + self.destinations.remove(destination) + return + self.destinations = [] class ObjectBlock(): @@ -107,9 +111,7 @@ def __init__(self, source_widget, destination_widget): self.destination.source = self.source def unlink(self): - self.get_parent().remove_child(self.bt_unlink) - self.get_parent().remove_child(self) - self.source.unlink() + self.source.unlink(self.destination) self.destination.unlink() diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 9682298c..10133ba1 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -68,7 +68,7 @@ class InputView(FBD_model.Input, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None previous_value = None - + link = None def __init__(self, name, *args, **kwargs): gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) self.placeholder = gui.SvgRectangle(0, 0, 0, 0) @@ -91,9 +91,8 @@ def set_default_look(self): self.label.set_fill('black') def unlink(self): - ret = super().unlink() + FBD_model.Input.unlink(self) self.set_default_look() - return ret def get_value(self): v = FBD_model.Input.get_value(self) @@ -115,6 +114,8 @@ def set_size(self, width, height): @gui.decorate_event def onpositionchanged(self): + if not self.link is None: + self.link.update_path() return () @@ -137,6 +138,21 @@ def __init__(self, name, *args, **kwargs): FBD_model.Output.__init__(self, name, *args, **kwargs) + def link(self, destination, container): + link = LinkView(self, destination, container) + container.append(link) + bt_unlink = Unlink() + container.append(bt_unlink) + link.set_unlink_button(bt_unlink) + + destination.link = link + FBD_model.Output.link(self, destination) + + def unlink(self, destination = None): + if not destination is None: + destination.link = None + FBD_model.Output.unlink(self, destination) + def set_size(self, width, height): if self.placeholder: self.placeholder.set_size(width, height) @@ -157,6 +173,8 @@ def set_value(self, value): @gui.decorate_event def onpositionchanged(self): + for destination in self.destinations: + destination.link.update_path() return () @@ -191,8 +209,6 @@ def __init__(self, source_widget, destination_widget, container, *args, **kwargs self.set_stroke(1, 'black') self.set_fill('transparent') self.attributes['stroke-dasharray'] = "4 2" - self.source.onpositionchanged.do(self.update_path) - self.destination.onpositionchanged.do(self.update_path) #this is to prevent stopping elements drag when moving over a link self.style['pointer-events'] = 'none' @@ -597,11 +613,7 @@ def onselection_end(self, emitter, x, y): if self.selected_input != None and self.selected_output != None: if self.selected_input.is_linked(): return - link = LinkView(self.selected_output, self.selected_input, self) - self.append(link) - bt_unlink = Unlink() - self.append(bt_unlink) - link.set_unlink_button(bt_unlink) + self.selected_output.link(self.selected_input, self) def add_function_block(self, function_block): function_block.onclick.do(self.onfunction_block_clicked) From be374443b7d8ef321c376d6396c8e4cffbd4e67d Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 23 Nov 2022 16:47:03 +0100 Subject: [PATCH 095/110] FBD with remi events. --- editor/FBD_model.py | 20 +++++-- editor/FBD_view.py | 131 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 137 insertions(+), 14 deletions(-) diff --git a/editor/FBD_model.py b/editor/FBD_model.py index e9c7fc21..3d62eb76 100644 --- a/editor/FBD_model.py +++ b/editor/FBD_model.py @@ -20,6 +20,8 @@ def has_default(self): return not (self.default == inspect.Parameter.empty) def link(self, output): + if not issubclass(type(output), Output): + return self.source = output def is_linked(self): @@ -47,6 +49,8 @@ def set_value(self, value): self.value = value def link(self, destination): + if not issubclass(type(destination), Input): + return self.destinations.append(destination) def is_linked(self): @@ -63,9 +67,20 @@ class ObjectBlock(): name = None FBs = None #this is the list of member functions + inputs = None + outputs = None + def __init__(self, name): self.name = name self.FBs = {} + self.inputs = {} + self.outputs = {} + + def add_io(self, io): + if issubclass(type(io), Input): + self.inputs[io.name] = io + else: + self.outputs[io.name] = io class FunctionBlock(): @@ -97,7 +112,7 @@ def add_io(self, io): @decorate_process([]) def do(self): - return True, 28 + return None class Link(): @@ -107,9 +122,6 @@ def __init__(self, source_widget, destination_widget): self.source = source_widget self.destination = destination_widget - self.source.destinaton = self.destination - self.destination.source = self.source - def unlink(self): self.source.unlink(self.destination) self.destination.unlink() diff --git a/editor/FBD_view.py b/editor/FBD_view.py index 10133ba1..fa5d4fb4 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -68,7 +68,7 @@ class InputView(FBD_model.Input, gui.SvgSubcontainer, MixinPositionSize): placeholder = None label = None previous_value = None - link = None + link_view = None def __init__(self, name, *args, **kwargs): gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) self.placeholder = gui.SvgRectangle(0, 0, 0, 0) @@ -90,6 +90,10 @@ def set_default_look(self): self.label.style['cursor'] = 'pointer' self.label.set_fill('black') + def link(self, source, link_view): + FBD_model.Input.link(self, source) + self.link_view = link_view + def unlink(self): FBD_model.Input.unlink(self) self.set_default_look() @@ -114,8 +118,8 @@ def set_size(self, width, height): @gui.decorate_event def onpositionchanged(self): - if not self.link is None: - self.link.update_path() + if not self.link_view is None: + self.link_view.update_path() return () @@ -139,14 +143,13 @@ def __init__(self, name, *args, **kwargs): FBD_model.Output.__init__(self, name, *args, **kwargs) def link(self, destination, container): - link = LinkView(self, destination, container) - container.append(link) + link_view = LinkView(self, destination, container) + container.append(link_view) bt_unlink = Unlink() container.append(bt_unlink) - link.set_unlink_button(bt_unlink) - - destination.link = link + link_view.set_unlink_button(bt_unlink) FBD_model.Output.link(self, destination) + destination.link(self, link_view) def unlink(self, destination = None): if not destination is None: @@ -178,6 +181,72 @@ def onpositionchanged(self): return () +class InputEvent(InputView): + placeholder = None + label = None + event_callback = None + def __init__(self, name, event_callback, *args, **kwargs): + self.event_callback = event_callback + gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, 0, 0) + self.placeholder.set_stroke(1, 'black') + self.placeholder.set_fill("orange") + self.placeholder.style['cursor'] = 'pointer' + self.append(self.placeholder) + + self.label = gui.SvgText("100%", "50%", name) + self.label.attr_dominant_baseline = 'middle' + self.label.attr_text_anchor = "end" + self.label.style['cursor'] = 'pointer' + self.append(self.label) + + FBD_model.Output.__init__(self, name, *args, **kwargs) + + def link(self, source, link_view): + if not issubclass(type(source), OutputEvent): + return + self.placeholder.set_fill('green') + FBD_model.InputView.link(self, source, link_view) + + def unlink(self, destination = None): + self.placeholder.set_fill('orange') + FBD_model.InputView.unlink(self) + + +class OutputEvent(OutputView): + placeholder = None + label = None + event_connector = None + def __init__(self, name, event_connector, *args, **kwargs): + self.event_connector = event_connector + gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, 0, 0) + self.placeholder.set_stroke(1, 'black') + self.placeholder.set_fill("orange") + self.placeholder.style['cursor'] = 'pointer' + self.append(self.placeholder) + + self.label = gui.SvgText("100%", "50%", name) + self.label.attr_dominant_baseline = 'middle' + self.label.attr_text_anchor = "end" + self.label.style['cursor'] = 'pointer' + self.append(self.label) + + FBD_model.Output.__init__(self, name, *args, **kwargs) + + def link(self, destination, container): + if not issubclass(type(destination), InputEvent): + return + self.placeholder.set_fill('green') + gui.ClassEventConnector.do(self.event_connector, destination.event_callback) + OutputView.link(self, destination, container) + + def unlink(self, destination = None): + self.placeholder.set_fill('orange') + gui.ClassEventConnector.do(self, None) + FBD_model.Output.unlink(self, destination) + + class Unlink(gui.SvgSubcontainer): def __init__(self, x=0, y=0, w=15, h=15, *args, **kwargs): gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) @@ -308,6 +377,9 @@ class ObjectBlockView(FBD_model.ObjectBlock, gui.SvgSubcontainer, MoveableWidget reference_object = None + io_font_size = 12 + io_left_right_offset = 10 + def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): name = obj.__class__.__name__ self.reference_object = obj @@ -380,18 +452,48 @@ def add_fb_view(self, fb_view_instance): fb.on_drag.do(self.onfunction_block_position_changed) self.adjust_geometry() + def add_io_widget(self, widget): + widget.label.css_font_size = gui.to_pix(self.io_font_size) + widget.set_size(len(widget.name) * self.io_font_size, self.io_font_size) + + FBD_model.FunctionBlock.add_io(self, widget) + self.append(widget) + widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) + widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) + + self.adjust_geometry() + def onfunction_block_position_changed(self, emitter, x, y): emitter.adjust_geometry() self.adjust_geometry() def adjust_geometry(self): - #for fb in self.FBs.values(): - # fb.adjust_geometry() + w, h = self.get_size() + + i = 1 + for inp in self.inputs.values(): + inp.set_position(0, self.label_font_size + self.io_font_size*i) + inp.onpositionchanged() + i += 1 + + i = 1 + for o in self.outputs.values(): + ow, oh = o.get_size() + o.set_position(w - ow, self.label_font_size + self.io_font_size*i) + o.onpositionchanged() + i += 1 + gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) def set_position(self, x, y): for fb in self.FBs.values(): fb.onposition_changed() + + for inp in self.inputs.values(): + inp.onpositionchanged() + + for o in self.outputs.values(): + o.onpositionchanged() #w, h = self.get_size() #self.attr_viewBox = "%s %s %s %s"%(x, y, x+w, y+h) return gui.SvgSubcontainer.set_position(self, x, y) @@ -414,6 +516,15 @@ def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): ofbv = ObjectFunctionBlockView(self.reference_object, txt.set_value, "set_value", "set_value", self) self.add_fb_view(ofbv) + ie = InputEvent("onclicked", self.callback_test) + self.add_io_widget(ie) + + oe = OutputEvent("onclick", self.onclick) + self.add_io_widget(oe) + + def callback_test(self, emitter): + self.outline.set_stroke(2, 'red') + class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): From e2d6107d6d29a2c60a97b46f25429ac6f124ab3b Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 23 Nov 2022 23:20:37 +0100 Subject: [PATCH 096/110] FBD title is now linkable to events. --- editor/FBD_view.py | 84 ++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/editor/FBD_view.py b/editor/FBD_view.py index fa5d4fb4..34202307 100644 --- a/editor/FBD_view.py +++ b/editor/FBD_view.py @@ -113,7 +113,7 @@ def get_value(self): def set_size(self, width, height): if self.placeholder: - self.placeholder.set_size(width, height) + gui._MixinSvgSize.set_size(self.placeholder, width, height) return gui._MixinSvgSize.set_size(self, width, height) @gui.decorate_event @@ -153,7 +153,7 @@ def link(self, destination, container): def unlink(self, destination = None): if not destination is None: - destination.link = None + destination.link_view = None FBD_model.Output.unlink(self, destination) def set_size(self, width, height): @@ -177,7 +177,7 @@ def set_value(self, value): @gui.decorate_event def onpositionchanged(self): for destination in self.destinations: - destination.link.update_path() + destination.link_view.update_path() return () @@ -188,29 +188,34 @@ class InputEvent(InputView): def __init__(self, name, event_callback, *args, **kwargs): self.event_callback = event_callback gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) + self.placeholder = gui.SvgRectangle(0, 0, 0, 0) + self.append(self.placeholder) + + self.label = gui.SvgText("0%", "50%", name) + self.append(self.label) + + FBD_model.Input.__init__(self, name, *args, **kwargs) + self.set_default_look() + + def set_default_look(self): self.placeholder.set_stroke(1, 'black') self.placeholder.set_fill("orange") self.placeholder.style['cursor'] = 'pointer' - self.append(self.placeholder) - self.label = gui.SvgText("100%", "50%", name) self.label.attr_dominant_baseline = 'middle' - self.label.attr_text_anchor = "end" + self.label.attr_text_anchor = "start" self.label.style['cursor'] = 'pointer' - self.append(self.label) - - FBD_model.Output.__init__(self, name, *args, **kwargs) def link(self, source, link_view): if not issubclass(type(source), OutputEvent): return self.placeholder.set_fill('green') - FBD_model.InputView.link(self, source, link_view) + InputView.link(self, source, link_view) def unlink(self, destination = None): self.placeholder.set_fill('orange') - FBD_model.InputView.unlink(self) + InputView.unlink(self) class OutputEvent(OutputView): @@ -243,7 +248,7 @@ def link(self, destination, container): def unlink(self, destination = None): self.placeholder.set_fill('orange') - gui.ClassEventConnector.do(self, None) + gui.ClassEventConnector.do(self.event_connector, None) FBD_model.Output.unlink(self, destination) @@ -536,6 +541,8 @@ class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWi io_font_size = 12 io_left_right_offset = 10 + input_event = None + def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): FBD_model.FunctionBlock.__init__(self, name) gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) @@ -546,12 +553,22 @@ def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): self.outline.set_stroke(2, 'black') self.append(self.outline) - self.label = gui.SvgText("50%", 0, self.name) - self.label.attr_text_anchor = "middle" - self.label.attr_dominant_baseline = 'hanging' - self.label.css_font_size = gui.to_pix(self.label_font_size) - self.append(self.label) + self.input_event = InputEvent(self.name, self.do) + self.input_event.label.attr_text_anchor = "middle" + #self.input_event.label.attr_dominant_baseline = 'hanging' + self.input_event.label.css_font_size = gui.to_pix(self.io_font_size) + self.input_event.label.attr_x = "50%" + self.input_event.label.attr_y = "50%" + self.input_event.set_size(len(self.input_event.name) * self.io_font_size, self.io_font_size) + self.input_event.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) + self.input_event.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) + self.append(self.input_event) + + self.populate_io() + self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) + + def populate_io(self): #for all the outputs defined by decorator on FunctionBlock.do # add the related Outputs if hasattr(self.do, "_outputs"): @@ -562,8 +579,6 @@ def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): for arg in signature.parameters: self.add_io_widget(InputView(arg, default = signature.parameters[arg].default)) - self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) - def calc_height(self): inputs_count = 0 if self.inputs == None else len(self.inputs) outputs_count = 0 if self.outputs == None else len(self.outputs) @@ -604,6 +619,10 @@ def adjust_geometry(self): gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) w, h = self.get_size() + if not self.input_event is None: + iew, ieh = self.input_event.get_size() + self.input_event.set_position((w-iew)/2, 0) + i = 1 for inp in self.inputs.values(): inp.set_position(0, self.label_font_size + self.io_font_size*i) @@ -641,41 +660,26 @@ def __init__(self, obj, method, method_name, name, container, x = 10, y = 10, *a self.reference_object = obj self.method = method self.method_name = method_name - FBD_model.FunctionBlock.__init__(self, name) - gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) - MoveableWidget.__init__(self, container, *args, **kwargs) - - self.outline = gui.SvgRectangle(0, 0, "100%", "100%") - self.outline.set_fill('white') - self.outline.set_stroke(2, 'black') - self.append(self.outline) - - self.label = gui.SvgText("50%", 0, self.name) - self.label.attr_text_anchor = "middle" - self.label.attr_dominant_baseline = 'hanging' - self.label.css_font_size = gui.to_pix(self.label_font_size) - self.append(self.label) - + FunctionBlockView.__init__(self, name, container, x, y, *args, **kwargs) + #for all the outputs defined by decorator on FunctionBlock.do # add the related Outputs #if hasattr(self.do, "_outputs"): # for o in self.do._outputs: # self.add_io_widget(OutputView(o)) + def populate_io(self): signature = inspect.signature(getattr(self.reference_object, self.method_name)) for arg in signature.parameters: self.add_io_widget(InputView(arg, default=signature.parameters[arg].default)) self.add_io_widget(InputView('EN', default=False)) - #self.do = getattr(self.reference_object, self.method_name) - - self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) - def do(self, *args, **kwargs): if kwargs.get('EN') != None: if kwargs['EN'] == False: return - del kwargs['EN'] + if 'EN' in kwargs: + del kwargs['EN'] output = getattr(self.reference_object, self.method_name)(*args, **kwargs) """ #this is to populate outputs automatically @@ -724,7 +728,7 @@ def onselection_end(self, emitter, x, y): if self.selected_input != None and self.selected_output != None: if self.selected_input.is_linked(): return - self.selected_output.link(self.selected_input, self) + self.selected_output.link(self.selected_input, self) def add_function_block(self, function_block): function_block.onclick.do(self.onfunction_block_clicked) From 65f54b8fb9bec9983ace2ddee1cc0d874cb32754 Mon Sep 17 00:00:00 2001 From: Vlad Kolesnikov Date: Sat, 7 Jan 2023 06:23:05 +0000 Subject: [PATCH 097/110] Dynamic server address:port for the client websocket --- README.md | 9 ++-- remi/gui.py | 143 +++++++++++++++++++++++++++---------------------- remi/server.py | 47 ++++++++-------- 3 files changed, 110 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index ea328b04..51b54884 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

-Remi is a GUI library for Python applications that gets rendered in web browsers. +Remi is a GUI library for Python applications that gets rendered in web browsers. This allows you to access your interface locally and remotely.

@@ -166,7 +166,7 @@ Run the script. If it's all OK the GUI will be opened automatically in your brow You can customize optional parameters in the `start` call like: ```py -start(MyApp, address='127.0.0.1', port=8081, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True) +start(MyApp, address='127.0.0.1', port=8081, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True, dynamic_web_address=True) ``` Parameters: @@ -184,6 +184,8 @@ Additional Parameters: - certfile: SSL certificate filename - keyfile: SSL key file - ssl_version: authentication version (i.e. ssl.PROTOCOL_TLSv1_2). If None disables SSL encryption +- dynamic_web_address: set it to `True` if the server is not aware of the IP address and the URL the user will be opening the app with. +If so, the JavaScript code will use hostname and port in the browser. This parameter is `False` by default. All widgets constructors accept two standards**kwargs that are: - width: can be expressed as int (and is interpreted as a pixel) or as str (and you can specify the measuring unit like '10%') @@ -312,9 +314,10 @@ Remote access === If you are using your REMI app remotely, with a DNS and behind a firewall, you can specify special parameters in the `start` call: - **port**: HTTP server port. Don't forget to NAT this port on your router; +- **dynamic_web_address**: set to `True` if the JavaScript code should use the actual URL's host and port for connecting back to the app, instead of provided IP address. This parameter is `False` by default. ```py -start(MyApp, address='0.0.0.0', port=8081) +start(MyApp, address='0.0.0.0', port=8081, dynamic_web_address=True) ``` diff --git a/remi/gui.py b/remi/gui.py index f363b849..3fe9630c 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -121,7 +121,7 @@ def a_method_not_builtin(obj): # this is implicit in predicate #if not hasattr(method, '__is_event'): # continue - + _event_info = None if hasattr(method, "_event_info"): _event_info = method._event_info @@ -160,7 +160,7 @@ def do(self, callback, *userdata, **kwuserdata): 'emitter_identifier': self.event_source_instance.identifier, 'event_name': self.event_name} + \ ("event.stopPropagation();" if js_stop_propagation else "") + \ ("event.preventDefault();" if js_prevent_default else "") - + self.callback = callback def __call__(self, *args, **kwargs): @@ -220,7 +220,7 @@ def add_annotation(method): def editor_attribute_decorator(group, description, _type, additional_data): - def add_annotation(prop): + def add_annotation(prop): setattr(prop, "editor_attributes", {'description': description, 'type': _type, 'group': group, 'additional_data': additional_data}) return prop return add_annotation @@ -407,8 +407,8 @@ def _set_updated(self): self.style.align_version() def disable_refresh(self): - """ Prevents the parent widgets to be notified about an update. - This is required to improve performances in case of widgets updated + """ Prevents the parent widgets to be notified about an update. + This is required to improve performances in case of widgets updated multiple times in a procedure. """ self.refresh_enabled = False @@ -418,7 +418,7 @@ def enable_refresh(self): def disable_update(self): """ Prevents clients updates. Remi will not send websockets update messages. - The widgets are however iternally updated. So if the user updates the + The widgets are however iternally updated. So if the user updates the webpage, the update is shown. """ self.ignore_update = True @@ -915,7 +915,7 @@ def set_style(self, style): self.style[k.strip()] = v.strip() def set_enabled(self, enabled): - """ Sets the enabled status. + """ Sets the enabled status. If a widget is disabled the user iteraction is not allowed Args: @@ -1399,7 +1399,20 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que var self = this; try{ - this._ws = new WebSocket(ws_wss + '://%(host)s/'); + host = '%(host)s' + if (host !== ''){ + wss_url = `${ws_wss}://${host}/` + } + else{ + host = document.location.host; + port = document.location.port; + if (port != ''){ + port = `:${port}`; + } + wss_url = `${ws_wss}://${document.location.host}${port}/`; + } + + this._ws = new WebSocket(wss_url); console.debug('opening websocket'); this._ws.onopen = function(evt){ @@ -1612,7 +1625,7 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); return false; }; - + window.remi = new Remi(); """ % {'host':net_interface_ip, @@ -1803,7 +1816,7 @@ def set_row_sizes(self, values): values (iterable of int or str): values are treated as percentage. """ self.css_grid_template_rows = ' '.join(map(lambda value: (str(value) if str(value).endswith('%') else str(value) + '%') , values)) - + def set_column_gap(self, value): """Sets the gap value between columns @@ -1987,7 +2000,7 @@ class AsciiContainer(Container): def __init__(self, *args, **kwargs): Container.__init__(self, *args, **kwargs) self.css_position = 'relative' - + def set_from_asciiart(self, asciipattern, gap_horizontal=0, gap_vertical=0): """ asciipattern (str): a multiline string representing the layout @@ -2016,24 +2029,24 @@ def set_from_asciiart(self, asciipattern, gap_horizontal=0, gap_vertical=0): for column in columns: widget_key = column.strip() widget_width = float(len(column)) - + if not widget_key in list(self.widget_layout_map.keys()): #width is calculated in percent # height is instead initialized at 1 and incremented by 1 each row the key is present # at the end of algorithm the height will be converted in percent - self.widget_layout_map[widget_key] = { 'width': "%.2f%%"%float(widget_width / (row_width) * 100.0 - gap_horizontal), - 'height':1, - 'top':"%.2f%%"%float(row_index / (layout_height_in_chars) * 100.0 + (gap_vertical/2.0)), + self.widget_layout_map[widget_key] = { 'width': "%.2f%%"%float(widget_width / (row_width) * 100.0 - gap_horizontal), + 'height':1, + 'top':"%.2f%%"%float(row_index / (layout_height_in_chars) * 100.0 + (gap_vertical/2.0)), 'left':"%.2f%%"%float(left_value / (row_width) * 100.0 + (gap_horizontal/2.0))} else: self.widget_layout_map[widget_key]['height'] += 1 - + left_value += widget_width row_index += 1 #converting height values in percent string for key in self.widget_layout_map.keys(): - self.widget_layout_map[key]['height'] = "%.2f%%"%float(self.widget_layout_map[key]['height'] / (layout_height_in_chars) * 100.0 - gap_vertical) + self.widget_layout_map[key]['height'] = "%.2f%%"%float(self.widget_layout_map[key]['height'] / (layout_height_in_chars) * 100.0 - gap_vertical) for key in self.widget_layout_map.keys(): self.set_widget_layout(key) @@ -2079,7 +2092,7 @@ def resize_tab_titles(self): if not l is None: last_tab_w=100.0-tab_w*(nch-1) l.set_size("%.1f%%" % last_tab_w, "auto") - + def append(self, widget, key=''): """ Adds a new tab. @@ -2850,7 +2863,7 @@ def onchange(self, value): class DropDownItem(Widget, _MixinTextualWidget): """item widget for the DropDown""" @property - @editor_attribute_decorator("WidgetSpecific", '''The value returned to the DropDown in onchange event. + @editor_attribute_decorator("WidgetSpecific", '''The value returned to the DropDown in onchange event. By default it corresponds to the displayed text, unsless it is changes.''', str, {}) def value(self): return unescape(self.attributes.get('value', '').replace(' ', ' ')) @value.setter @@ -3015,7 +3028,7 @@ def __init__(self, n_rows=2, n_columns=2, use_title=True, editable=False, *args, kwargs: See Container.__init__() """ self.__column_count = 0 - self.__use_title = use_title + self.__use_title = use_title super(TableWidget, self).__init__(*args, **kwargs) self._editable = editable self.set_use_title(use_title) @@ -3403,7 +3416,7 @@ def __init__(self, default_value=0, min_value=0, max_value=65535, step=1, allow_ "if(key==13){var params={};params['value']=document.getElementById('%(id)s').value;" \ "remi.sendCallbackParam('%(id)s','%(evt)s',params); return true;}" \ "return false;" % {'id': self.identifier, 'evt': self.EVENT_ONCHANGE} - + @decorate_set_on_listener("(self, emitter, value)") @decorate_event def onchange(self, value): @@ -3423,7 +3436,7 @@ def onchange(self, value): #this is to force update in case a value out of limits arrived # and the limiting ended up with the same previous value stored in self.attributes - # In this case the limitation gets not updated in browser + # In this case the limitation gets not updated in browser # (because not triggering is_changed). So the update is forced. if _type(value) != _value: self.attributes.onchange() @@ -3572,10 +3585,10 @@ def attr_value(self, value): self.attributes['value'] = str(value) @property @editor_attribute_decorator("WidgetSpecific", '''Defines the datalist.''', str, {}) - def attr_datalist_identifier(self): + def attr_datalist_identifier(self): return self.attributes.get('list', '0') @attr_datalist_identifier.setter - def attr_datalist_identifier(self, value): + def attr_datalist_identifier(self, value): if isinstance(value, Datalist): value = value.identifier self.attributes['list'] = value @@ -3589,11 +3602,11 @@ def attr_input_type(self, value): self.attributes['type'] = str(value) def __init__(self, default_value="", input_type="text", *args, **kwargs): """ Args: - selection_type (str): text, search, url, tel, email, date, month, week, time, datetime-local, number, range, color. + selection_type (str): text, search, url, tel, email, date, month, week, time, datetime-local, number, range, color. kwargs: See Widget.__init__() """ super(SelectionInput, self).__init__(input_type, default_value, *args, **kwargs) - + self.attributes[Widget.EVENT_ONCHANGE] = \ "var params={};params['value']=document.getElementById('%(emitter_identifier)s').value;" \ "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);"% \ @@ -3627,7 +3640,7 @@ def get_datalist_identifier(self): class SelectionInputWidget(Container): - datalist = None #the internal Datalist + datalist = None #the internal Datalist selection_input = None #the internal selection_input @property @@ -3648,14 +3661,14 @@ def __init__(self, iterable_of_str=None, default_value="", input_type='text', *a if iterable_of_str: options = list(map(DatalistItem, iterable_of_str)) self.datalist = Datalist(options) - self.selection_input = SelectionInput(default_value, input_type, style={'top':'0px', + self.selection_input = SelectionInput(default_value, input_type, style={'top':'0px', 'left':'0px', 'bottom':'0px', 'right':'0px'}) self.selection_input.set_datalist_identifier(self.datalist.identifier) self.append([self.datalist, self.selection_input]) self.selection_input.oninput.do(self.oninput) - + def set_value(self, value): - """ + """ Sets the value of the widget Args: value (str): the string value @@ -3672,7 +3685,7 @@ def get_value(self): @decorate_set_on_listener("(self, emitter, value)") @decorate_event def oninput(self, emitter, value): - """ + """ This event occurs when user inputs a new value Returns: value (str): the string value @@ -3709,7 +3722,7 @@ def multiple_selection(self, value): self._multiple_selection = value @editor_attribute_decorator("WidgetSpecific", '''Defines the actual navigator location.''', str, {}) def selection_folder(self): return self._selection_folder @selection_folder.setter - def selection_folder(self, value): + def selection_folder(self, value): # fixme: we should use full paths and not all this chdir stuff self.chdir(value) # move to actual working directory @@ -3849,7 +3862,7 @@ def chdir(self, directory): @decorate_event def on_folder_item_selected(self, folderitem): """ This event occurs when an element in the list is selected - Returns the newly selected element of type FileFolderItem(or None if it was not selectable) + Returns the newly selected element of type FileFolderItem(or None if it was not selectable) and the list of selected elements of type str. """ if folderitem.isFolder and (not self.allow_folder_selection): @@ -4039,7 +4052,7 @@ def __init__(self, text='', *args, **kwargs): self.attributes['treeopen'] = 'false' self.attributes['has-subtree'] = 'false' self.onclick.do(None, js_stop_propagation=True) - + def append(self, value, key=''): if self.sub_container is None: self.attributes['has-subtree'] = 'true' @@ -4065,11 +4078,11 @@ class FileUploader(Container): implements the onsuccess and onfailed events. """ @property - @editor_attribute_decorator("WidgetSpecific",'''If True multiple files can be + @editor_attribute_decorator("WidgetSpecific",'''If True multiple files can be selected at the same time''', bool, {}) def multiple_selection_allowed(self): return ('multiple' in self.__dict__.keys()) @multiple_selection_allowed.setter - def multiple_selection_allowed(self, value): + def multiple_selection_allowed(self, value): if value: self.__dict__["multiple"] = "multiple" else: @@ -4080,7 +4093,7 @@ def multiple_selection_allowed(self, value): @editor_attribute_decorator("WidgetSpecific", '''Defines the path where to save the file''', str, {}) def savepath(self): return self._savepath @savepath.setter - def savepath(self, value): + def savepath(self, value): self._savepath = value def __init__(self, savepath='./', multiple_selection_allowed=False, accepted_files='*.*', *args, **kwargs): @@ -4232,7 +4245,7 @@ def attr_stroke(self): return self.attributes.get('stroke', None) @attr_stroke.setter def attr_stroke(self, value): self.attributes['stroke'] = str(value) @attr_stroke.deleter - def attr_stroke(self): del self.attributes['stroke'] + def attr_stroke(self): del self.attributes['stroke'] @property @editor_attribute_decorator("WidgetSpecific", '''Stroke width for svg elements.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) @@ -4240,7 +4253,7 @@ def attr_stroke_width(self): return self.attributes.get('stroke-width', None) @attr_stroke_width.setter def attr_stroke_width(self, value): self.attributes['stroke-width'] = str(value) @attr_stroke_width.deleter - def attr_stroke_width(self): del self.attributes['stroke-width'] + def attr_stroke_width(self): del self.attributes['stroke-width'] def set_stroke(self, width=1, color='black'): """Sets the stroke properties. @@ -4260,7 +4273,7 @@ def css_transform(self): return self.style.get('transform', None) @css_transform.setter def css_transform(self, value): self.style['transform'] = str(value) @css_transform.deleter - def css_transform(self): del self.style['transform'] + def css_transform(self): del self.style['transform'] @property @editor_attribute_decorator("Transformation", '''Transform origin as percent or absolute x,y pair value or ['center','top','bottom','left','right'] .''', str, {}) @@ -4268,7 +4281,7 @@ def css_transform_origin(self): return self.style.get('transform-origin', None) @css_transform_origin.setter def css_transform_origin(self, value): self.style['transform-origin'] = str(value) @css_transform_origin.deleter - def css_transform_origin(self): del self.style['transform-origin'] + def css_transform_origin(self): del self.style['transform-origin'] @property @editor_attribute_decorator("Transformation", '''Alters the behaviour of tranform and tranform-origin by defining the transform box.''', 'DropDown', {'possible_values': ('content-box','border-box','fill-box','stroke-box','view-box')}) @@ -4276,7 +4289,7 @@ def css_transform_box(self): return self.style.get('transform-box', None) @css_transform_box.setter def css_transform_box(self, value): self.style['transform-box'] = str(value) @css_transform_box.deleter - def css_transform_box(self): del self.style['transform-box'] + def css_transform_box(self): del self.style['transform-box'] class _MixinSvgFill(): @@ -4286,7 +4299,7 @@ def attr_fill(self): return self.attributes.get('fill', None) @attr_fill.setter def attr_fill(self, value): self.attributes['fill'] = str(value) @attr_fill.deleter - def attr_fill(self): del self.attributes['fill'] + def attr_fill(self): del self.attributes['fill'] @property @editor_attribute_decorator("WidgetSpecific", '''Fill opacity for svg elements.''', float, {'possible_values': '', 'min': 0.0, 'max': 1.0, 'default': 1.0, 'step': 0.1}) @@ -4294,7 +4307,7 @@ def attr_fill_opacity(self): return self.attributes.get('fill-opacity', None) @attr_fill_opacity.setter def attr_fill_opacity(self, value): self.attributes['fill-opacity'] = str(value) @attr_fill_opacity.deleter - def attr_fill_opacity(self): del self.attributes['fill-opacity'] + def attr_fill_opacity(self): del self.attributes['fill-opacity'] def set_fill(self, color='black'): """Sets the fill color. @@ -4355,7 +4368,7 @@ def set_size(self, w, h): class SvgStop(Tag): """ """ - + @property @editor_attribute_decorator("WidgetSpecific", '''Gradient color''', 'ColorPicker', {}) def css_stop_color(self): return self.style.get('stop-color', None) @@ -4393,7 +4406,7 @@ class SvgGradientLinear(Tag): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_x1(self): return self.attributes.get('x1', None) @attr_x1.setter - def attr_x1(self, value): + def attr_x1(self, value): self.attributes['x1'] = str(value) if not self.attributes['x1'][-1] == '%': self.attributes['x1'] = self.attributes['x1'] + '%' @@ -4402,16 +4415,16 @@ def attr_x1(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_y1(self): return self.attributes.get('y1', None) @attr_y1.setter - def attr_y1(self, value): + def attr_y1(self, value): self.attributes['y1'] = str(value) if not self.attributes['y1'][-1] == '%': self.attributes['y1'] = self.attributes['y1'] + '%' - + @property @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_x2(self): return self.attributes.get('x2', None) @attr_x2.setter - def attr_x2(self, value): + def attr_x2(self, value): self.attributes['x2'] = str(value) if not self.attributes['x2'][-1] == '%': self.attributes['x2'] = self.attributes['x2'] + '%' @@ -4420,7 +4433,7 @@ def attr_x2(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_y2(self): return self.attributes.get('y2', None) @attr_y2.setter - def attr_y2(self, value): + def attr_y2(self, value): self.attributes['y2'] = str(value) if not self.attributes['y2'][-1] == '%': self.attributes['y2'] = self.attributes['y2'] + '%' @@ -4440,7 +4453,7 @@ class SvgGradientRadial(Tag): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_cx(self): return self.attributes.get('cx', None) @attr_cx.setter - def attr_cx(self, value): + def attr_cx(self, value): self.attributes['cx'] = str(value) if not self.attributes['cx'][-1] == '%': self.attributes['cx'] = self.attributes['cx'] + '%' @@ -4449,7 +4462,7 @@ def attr_cx(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_cy(self): return self.attributes.get('cy', None) @attr_cy.setter - def attr_cy(self, value): + def attr_cy(self, value): self.attributes['cy'] = str(value) if not self.attributes['cy'][-1] == '%': self.attributes['cy'] = self.attributes['cy'] + '%' @@ -4458,7 +4471,7 @@ def attr_cy(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_fx(self): return self.attributes.get('fx', None) @attr_fx.setter - def attr_fx(self, value): + def attr_fx(self, value): self.attributes['fx'] = str(value) if not self.attributes['fx'][-1] == '%': self.attributes['fx'] = self.attributes['fx'] + '%' @@ -4467,7 +4480,7 @@ def attr_fx(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_fy(self): return self.attributes.get('fy', None) @attr_fy.setter - def attr_fy(self, value): + def attr_fy(self, value): self.attributes['fy'] = str(value) if not self.attributes['fy'][-1] == '%': self.attributes['fy'] = self.attributes['fy'] + '%' @@ -4476,7 +4489,7 @@ def attr_fy(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient radius value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_r(self): return self.attributes.get('r', None) @attr_r.setter - def attr_r(self, value): + def attr_r(self, value): self.attributes['r'] = str(value) if not self.attributes['r'][-1] == '%': self.attributes['r'] = self.attributes['r'] + '%' @@ -4496,7 +4509,7 @@ class SvgDefs(Tag): def __init__(self, *args, **kwargs): super(SvgDefs, self).__init__(*args, **kwargs) self.type = 'defs' - + class Svg(Container): """svg widget - is a container for graphic widgets such as SvgCircle, SvgLine and so on.""" @@ -4506,7 +4519,7 @@ def attr_preserveAspectRatio(self): return self.attributes.get('preserveAspectRa @attr_preserveAspectRatio.setter def attr_preserveAspectRatio(self, value): self.attributes['preserveAspectRatio'] = str(value) @attr_preserveAspectRatio.deleter - def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] + def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] @property @editor_attribute_decorator("WidgetSpecific",'''viewBox of the svg drawing. es='x, y, width, height' ''', 'str', {}) @@ -4514,7 +4527,7 @@ def attr_viewBox(self): return self.attributes.get('viewBox', None) @attr_viewBox.setter def attr_viewBox(self, value): self.attributes['viewBox'] = str(value) @attr_viewBox.deleter - def attr_viewBox(self): del self.attributes['viewBox'] + def attr_viewBox(self): del self.attributes['viewBox'] def __init__(self, *args, **kwargs): """ @@ -4523,7 +4536,7 @@ def __init__(self, *args, **kwargs): """ super(Svg, self).__init__(*args, **kwargs) self.type = 'svg' - + def set_viewbox(self, x, y, w, h): """Sets the origin and size of the viewbox, describing a virtual view area. @@ -4599,7 +4612,7 @@ def attr_preserveAspectRatio(self): return self.attributes.get('preserveAspectRa @attr_preserveAspectRatio.setter def attr_preserveAspectRatio(self, value): self.attributes['preserveAspectRatio'] = str(value) @attr_preserveAspectRatio.deleter - def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] + def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] @property @editor_attribute_decorator("WidgetSpecific", '''Image data or url or a base64 data string, html attribute xlink:href''', 'base64_image', {}) @@ -4607,7 +4620,7 @@ def image_data(self): return self.attributes.get('xlink:href', '') @image_data.setter def image_data(self, value): self.attributes['xlink:href'] = str(value) @image_data.deleter - def image_data(self): del self.attributes['xlink:href'] + def image_data(self): del self.attributes['xlink:href'] def __init__(self, image_data='', x=0, y=0, w=100, h=100, *args, **kwargs): """ @@ -4785,7 +4798,7 @@ class SvgPolyline(Widget, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @editor_attribute_decorator("WidgetSpecific",'''Defines the maximum values count.''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) def maxlen(self): return self.__maxlen @maxlen.setter - def maxlen(self, value): + def maxlen(self, value): self.__maxlen = int(value) self.coordsX = collections.deque(maxlen=self.__maxlen) self.coordsY = collections.deque(maxlen=self.__maxlen) @@ -4822,7 +4835,7 @@ def attr_textLength(self): return self.attributes.get('textLength', None) @attr_textLength.setter def attr_textLength(self, value): self.attributes['textLength'] = str(value) @attr_textLength.deleter - def attr_textLength(self): del self.attributes['textLength'] + def attr_textLength(self): del self.attributes['textLength'] @property @editor_attribute_decorator("WidgetSpecific", '''Controls how text is stretched to fit the length.''', 'DropDown', {'possible_values': ('spacing','spacingAndGlyphs')}) @@ -4830,7 +4843,7 @@ def attr_lengthAdjust(self): return self.attributes.get('lengthAdjust', None) @attr_lengthAdjust.setter def attr_lengthAdjust(self, value): self.attributes['lengthAdjust'] = str(value) @attr_lengthAdjust.deleter - def attr_lengthAdjust(self): del self.attributes['lengthAdjust'] + def attr_lengthAdjust(self): del self.attributes['lengthAdjust'] @property @editor_attribute_decorator("WidgetSpecific", '''Rotation angle for svg elements.''', float, {'possible_values': '', 'min': -360.0, 'max': 360.0, 'default': 1.0, 'step': 0.1}) @@ -4838,7 +4851,7 @@ def attr_rotate(self): return self.attributes.get('rotate', None) @attr_rotate.setter def attr_rotate(self, value): self.attributes['rotate'] = str(value) @attr_rotate.deleter - def attr_rotate(self): del self.attributes['rotate'] + def attr_rotate(self): del self.attributes['rotate'] @property @editor_attribute_decorator("WidgetSpecific", '''Description.''', 'DropDown', {'possible_values': ('start', 'middle', 'end')}) diff --git a/remi/server.py b/remi/server.py index cd451489..e7212b4a 100644 --- a/remi/server.py +++ b/remi/server.py @@ -208,7 +208,7 @@ def send_message(self, message): if not self.handshake_done: self._log.warning("ignoring message %s (handshake not done)" % message[:10]) return False - + if message[0] == "2": i = 0 @@ -259,8 +259,8 @@ def handshake(self): self.request.sendall(response.encode("utf-8")) self.handshake_done = True - #if an update happens since the websocket connection to its handshake, - # it gets not displayed. it is required to inform App about handshake done, + #if an update happens since the websocket connection to its handshake, + # it gets not displayed. it is required to inform App about handshake done, # to get a full refresh clients[self.session].websocket_handshake_done(self) @@ -392,7 +392,7 @@ def _instance(self): self.update_interval = self.server.update_interval from remi import gui - + head = gui.HEAD(self.server.title) # use the default css, but append a version based on its hash, to stop browser caching head.add_child('internal_css', "\n") @@ -437,8 +437,8 @@ def _instance(self): self._need_update_flag = client._need_update_flag if hasattr(client, '_update_thread'): self._update_thread = client._update_thread - - net_interface_ip = self._net_interface_ip() + + net_interface_ip = self._net_interface_ip() if not self.server.dynamic_address else '' websocket_timeout_timer_ms = str(self.server.websocket_timeout_timer_ms) pending_messages_queue_length = str(self.server.pending_messages_queue_length) self.page.children['head'].set_internal_js(str(id(self)), net_interface_ip, pending_messages_queue_length, websocket_timeout_timer_ms) @@ -466,7 +466,7 @@ def _idle_loop(self): try: self.do_gui_update() except Exception: - self._log.error('''exception during gui update. It is advisable to + self._log.error('''exception during gui update. It is advisable to use App.update_lock using external threads.''', exc_info=True) def idle(self): @@ -476,7 +476,7 @@ def idle(self): def _need_update(self, emitter=None, child_ignore_update=False): if child_ignore_update: - #the widgets tree is processed to make it available for a intentional + #the widgets tree is processed to make it available for a intentional # client update and to reset the changed flags of changed widget. # Otherwise it will be updated on next update cycle. changed_widget_dict = {} @@ -489,7 +489,7 @@ def _need_update(self, emitter=None, child_ignore_update=False): else: #will be updated after idle loop self._need_update_flag = True - + def do_gui_update(self): """ This method gets called also by Timer, a new thread, and so needs to lock the update """ @@ -518,7 +518,7 @@ def set_root_widget(self, widget): msg = "0" + self.root.identifier + ',' + to_websocket(self._overload(self.page.children['body'].innerHTML({}), filename="internal")) self._send_spontaneous_websocket_message(msg) - + def _send_spontaneous_websocket_message(self, message): for ws in list(self.websockets): # noinspection PyBroadException @@ -537,7 +537,7 @@ def _send_spontaneous_websocket_message(self, message): pass # happens when there are multiple clients else: ws.close(terminate_server=False) - + def execute_javascript(self, code): self._send_spontaneous_websocket_message(_MSG_JS + code) @@ -616,7 +616,7 @@ def do_GET(self): # if this is a ws req, instance a ws handler, add it to App's ws list, return if "Upgrade" in self.headers: if self.headers['Upgrade'].lower() == 'websocket': - #passing arguments to websocket handler, otherwise it will lost the last message, + #passing arguments to websocket handler, otherwise it will lost the last message, # and will be unable to handshake ws = WebSocketsHandler(self.headers, self.request, self.client_address, self.server) return @@ -673,7 +673,7 @@ def _get_static_file(self, filename): if not key in paths: return None return os.path.join(paths[key], path) - + def _overload(self, data, **kwargs): """Used to overload the content before sent back to client""" return data @@ -688,14 +688,14 @@ def _process_all(self, func, **kwargs): self.send_header("Set-Cookie", "remi_session=%s; SameSite=Lax"%(self.session)) self.send_header('Content-type', 'text/html') self.end_headers() - + with self.update_lock: # render the HTML page_content = self.page.repr() self.wfile.write(encode_text("\n")) self.wfile.write(encode_text(self._overload(page_content, filename="internal"))) - + elif static_file: filename = self._get_static_file(static_file.groups()[0]) if not filename: @@ -762,7 +762,7 @@ def onload(self, emitter): def onerror(self, message, source, lineno, colno, error): """ WebPage Event that occurs on webpage errors """ - self._log.debug("""App.onerror event occurred in webpage: + self._log.debug("""App.onerror event occurred in webpage: \nMESSAGE:%s\nSOURCE:%s\nLINENO:%s\nCOLNO:%s\ERROR:%s\n"""%(message, source, lineno, colno, error)) def ononline(self, emitter): @@ -794,7 +794,9 @@ class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer): def __init__(self, server_address, RequestHandlerClass, auth, multiple_instance, enable_file_cache, update_interval, websocket_timeout_timer_ms, pending_messages_queue_length, - title, server_starter_instance, certfile, keyfile, ssl_version, *userdata): + title, server_starter_instance, certfile, keyfile, ssl_version, + dynamic_address, + *userdata): HTTPServer.__init__(self, server_address, RequestHandlerClass) self.auth = auth self.multiple_instance = multiple_instance @@ -804,6 +806,7 @@ def __init__(self, server_address, RequestHandlerClass, self.pending_messages_queue_length = pending_messages_queue_length self.title = title self.server_starter_instance = server_starter_instance + self.dynamic_address = dynamic_address self.userdata = userdata self.certfile = certfile @@ -817,8 +820,8 @@ class Server(object): # noinspection PyShadowingNames def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=0, username=None, password=None, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True, - websocket_timeout_timer_ms=1000, pending_messages_queue_length=1000, - certfile=None, keyfile=None, ssl_version=None, userdata=()): + websocket_timeout_timer_ms=1000, pending_messages_queue_length=1000, + certfile=None, keyfile=None, ssl_version=None, userdata=(), dynamic_address=False): self._gui = gui_class self._title = title or gui_class.__name__ @@ -837,6 +840,7 @@ def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=0, self._keyfile = keyfile self._ssl_version = ssl_version self._userdata = userdata + self._dynamic_address = dynamic_address if username and password: self._auth = base64.b64encode(encode_text("%s:%s" % (username, password))) else: @@ -866,8 +870,9 @@ def start(self): self._sserver = ThreadedHTTPServer((self._address, self._sport), self._gui, self._auth, self._multiple_instance, self._enable_file_cache, self._update_interval, self._websocket_timeout_timer_ms, - self._pending_messages_queue_length, self._title, - self, self._certfile, self._keyfile, self._ssl_version, *self._userdata) + self._pending_messages_queue_length, self._title, + self, self._certfile, self._keyfile, self._ssl_version, self._dynamic_address, + *self._userdata) shost, sport = self._sserver.socket.getsockname()[:2] self._log.info('Started httpserver http://%s:%s/'%(shost,sport)) # when listening on multiple net interfaces the browsers connects to localhost From c84a6ed2f26c490bd0cabc195bc4668700b3329e Mon Sep 17 00:00:00 2001 From: Vlad Kolesnikov Date: Sat, 7 Jan 2023 06:30:34 +0000 Subject: [PATCH 098/110] Undo auto-formatting --- remi/gui.py | 128 ++++++++++++++++++++++++------------------------- remi/server.py | 28 +++++------ 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 3fe9630c..5e6d75ec 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -121,7 +121,7 @@ def a_method_not_builtin(obj): # this is implicit in predicate #if not hasattr(method, '__is_event'): # continue - + _event_info = None if hasattr(method, "_event_info"): _event_info = method._event_info @@ -160,7 +160,7 @@ def do(self, callback, *userdata, **kwuserdata): 'emitter_identifier': self.event_source_instance.identifier, 'event_name': self.event_name} + \ ("event.stopPropagation();" if js_stop_propagation else "") + \ ("event.preventDefault();" if js_prevent_default else "") - + self.callback = callback def __call__(self, *args, **kwargs): @@ -220,7 +220,7 @@ def add_annotation(method): def editor_attribute_decorator(group, description, _type, additional_data): - def add_annotation(prop): + def add_annotation(prop): setattr(prop, "editor_attributes", {'description': description, 'type': _type, 'group': group, 'additional_data': additional_data}) return prop return add_annotation @@ -407,8 +407,8 @@ def _set_updated(self): self.style.align_version() def disable_refresh(self): - """ Prevents the parent widgets to be notified about an update. - This is required to improve performances in case of widgets updated + """ Prevents the parent widgets to be notified about an update. + This is required to improve performances in case of widgets updated multiple times in a procedure. """ self.refresh_enabled = False @@ -418,7 +418,7 @@ def enable_refresh(self): def disable_update(self): """ Prevents clients updates. Remi will not send websockets update messages. - The widgets are however iternally updated. So if the user updates the + The widgets are however iternally updated. So if the user updates the webpage, the update is shown. """ self.ignore_update = True @@ -915,7 +915,7 @@ def set_style(self, style): self.style[k.strip()] = v.strip() def set_enabled(self, enabled): - """ Sets the enabled status. + """ Sets the enabled status. If a widget is disabled the user iteraction is not allowed Args: @@ -1625,7 +1625,7 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); return false; }; - + window.remi = new Remi(); """ % {'host':net_interface_ip, @@ -1816,7 +1816,7 @@ def set_row_sizes(self, values): values (iterable of int or str): values are treated as percentage. """ self.css_grid_template_rows = ' '.join(map(lambda value: (str(value) if str(value).endswith('%') else str(value) + '%') , values)) - + def set_column_gap(self, value): """Sets the gap value between columns @@ -2000,7 +2000,7 @@ class AsciiContainer(Container): def __init__(self, *args, **kwargs): Container.__init__(self, *args, **kwargs) self.css_position = 'relative' - + def set_from_asciiart(self, asciipattern, gap_horizontal=0, gap_vertical=0): """ asciipattern (str): a multiline string representing the layout @@ -2029,24 +2029,24 @@ def set_from_asciiart(self, asciipattern, gap_horizontal=0, gap_vertical=0): for column in columns: widget_key = column.strip() widget_width = float(len(column)) - + if not widget_key in list(self.widget_layout_map.keys()): #width is calculated in percent # height is instead initialized at 1 and incremented by 1 each row the key is present # at the end of algorithm the height will be converted in percent - self.widget_layout_map[widget_key] = { 'width': "%.2f%%"%float(widget_width / (row_width) * 100.0 - gap_horizontal), - 'height':1, - 'top':"%.2f%%"%float(row_index / (layout_height_in_chars) * 100.0 + (gap_vertical/2.0)), + self.widget_layout_map[widget_key] = { 'width': "%.2f%%"%float(widget_width / (row_width) * 100.0 - gap_horizontal), + 'height':1, + 'top':"%.2f%%"%float(row_index / (layout_height_in_chars) * 100.0 + (gap_vertical/2.0)), 'left':"%.2f%%"%float(left_value / (row_width) * 100.0 + (gap_horizontal/2.0))} else: self.widget_layout_map[widget_key]['height'] += 1 - + left_value += widget_width row_index += 1 #converting height values in percent string for key in self.widget_layout_map.keys(): - self.widget_layout_map[key]['height'] = "%.2f%%"%float(self.widget_layout_map[key]['height'] / (layout_height_in_chars) * 100.0 - gap_vertical) + self.widget_layout_map[key]['height'] = "%.2f%%"%float(self.widget_layout_map[key]['height'] / (layout_height_in_chars) * 100.0 - gap_vertical) for key in self.widget_layout_map.keys(): self.set_widget_layout(key) @@ -2092,7 +2092,7 @@ def resize_tab_titles(self): if not l is None: last_tab_w=100.0-tab_w*(nch-1) l.set_size("%.1f%%" % last_tab_w, "auto") - + def append(self, widget, key=''): """ Adds a new tab. @@ -2863,7 +2863,7 @@ def onchange(self, value): class DropDownItem(Widget, _MixinTextualWidget): """item widget for the DropDown""" @property - @editor_attribute_decorator("WidgetSpecific", '''The value returned to the DropDown in onchange event. + @editor_attribute_decorator("WidgetSpecific", '''The value returned to the DropDown in onchange event. By default it corresponds to the displayed text, unsless it is changes.''', str, {}) def value(self): return unescape(self.attributes.get('value', '').replace(' ', ' ')) @value.setter @@ -3028,7 +3028,7 @@ def __init__(self, n_rows=2, n_columns=2, use_title=True, editable=False, *args, kwargs: See Container.__init__() """ self.__column_count = 0 - self.__use_title = use_title + self.__use_title = use_title super(TableWidget, self).__init__(*args, **kwargs) self._editable = editable self.set_use_title(use_title) @@ -3416,7 +3416,7 @@ def __init__(self, default_value=0, min_value=0, max_value=65535, step=1, allow_ "if(key==13){var params={};params['value']=document.getElementById('%(id)s').value;" \ "remi.sendCallbackParam('%(id)s','%(evt)s',params); return true;}" \ "return false;" % {'id': self.identifier, 'evt': self.EVENT_ONCHANGE} - + @decorate_set_on_listener("(self, emitter, value)") @decorate_event def onchange(self, value): @@ -3436,7 +3436,7 @@ def onchange(self, value): #this is to force update in case a value out of limits arrived # and the limiting ended up with the same previous value stored in self.attributes - # In this case the limitation gets not updated in browser + # In this case the limitation gets not updated in browser # (because not triggering is_changed). So the update is forced. if _type(value) != _value: self.attributes.onchange() @@ -3585,10 +3585,10 @@ def attr_value(self, value): self.attributes['value'] = str(value) @property @editor_attribute_decorator("WidgetSpecific", '''Defines the datalist.''', str, {}) - def attr_datalist_identifier(self): + def attr_datalist_identifier(self): return self.attributes.get('list', '0') @attr_datalist_identifier.setter - def attr_datalist_identifier(self, value): + def attr_datalist_identifier(self, value): if isinstance(value, Datalist): value = value.identifier self.attributes['list'] = value @@ -3602,11 +3602,11 @@ def attr_input_type(self, value): self.attributes['type'] = str(value) def __init__(self, default_value="", input_type="text", *args, **kwargs): """ Args: - selection_type (str): text, search, url, tel, email, date, month, week, time, datetime-local, number, range, color. + selection_type (str): text, search, url, tel, email, date, month, week, time, datetime-local, number, range, color. kwargs: See Widget.__init__() """ super(SelectionInput, self).__init__(input_type, default_value, *args, **kwargs) - + self.attributes[Widget.EVENT_ONCHANGE] = \ "var params={};params['value']=document.getElementById('%(emitter_identifier)s').value;" \ "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);"% \ @@ -3640,7 +3640,7 @@ def get_datalist_identifier(self): class SelectionInputWidget(Container): - datalist = None #the internal Datalist + datalist = None #the internal Datalist selection_input = None #the internal selection_input @property @@ -3661,14 +3661,14 @@ def __init__(self, iterable_of_str=None, default_value="", input_type='text', *a if iterable_of_str: options = list(map(DatalistItem, iterable_of_str)) self.datalist = Datalist(options) - self.selection_input = SelectionInput(default_value, input_type, style={'top':'0px', + self.selection_input = SelectionInput(default_value, input_type, style={'top':'0px', 'left':'0px', 'bottom':'0px', 'right':'0px'}) self.selection_input.set_datalist_identifier(self.datalist.identifier) self.append([self.datalist, self.selection_input]) self.selection_input.oninput.do(self.oninput) - + def set_value(self, value): - """ + """ Sets the value of the widget Args: value (str): the string value @@ -3685,7 +3685,7 @@ def get_value(self): @decorate_set_on_listener("(self, emitter, value)") @decorate_event def oninput(self, emitter, value): - """ + """ This event occurs when user inputs a new value Returns: value (str): the string value @@ -3722,7 +3722,7 @@ def multiple_selection(self, value): self._multiple_selection = value @editor_attribute_decorator("WidgetSpecific", '''Defines the actual navigator location.''', str, {}) def selection_folder(self): return self._selection_folder @selection_folder.setter - def selection_folder(self, value): + def selection_folder(self, value): # fixme: we should use full paths and not all this chdir stuff self.chdir(value) # move to actual working directory @@ -3862,7 +3862,7 @@ def chdir(self, directory): @decorate_event def on_folder_item_selected(self, folderitem): """ This event occurs when an element in the list is selected - Returns the newly selected element of type FileFolderItem(or None if it was not selectable) + Returns the newly selected element of type FileFolderItem(or None if it was not selectable) and the list of selected elements of type str. """ if folderitem.isFolder and (not self.allow_folder_selection): @@ -4052,7 +4052,7 @@ def __init__(self, text='', *args, **kwargs): self.attributes['treeopen'] = 'false' self.attributes['has-subtree'] = 'false' self.onclick.do(None, js_stop_propagation=True) - + def append(self, value, key=''): if self.sub_container is None: self.attributes['has-subtree'] = 'true' @@ -4078,11 +4078,11 @@ class FileUploader(Container): implements the onsuccess and onfailed events. """ @property - @editor_attribute_decorator("WidgetSpecific",'''If True multiple files can be + @editor_attribute_decorator("WidgetSpecific",'''If True multiple files can be selected at the same time''', bool, {}) def multiple_selection_allowed(self): return ('multiple' in self.__dict__.keys()) @multiple_selection_allowed.setter - def multiple_selection_allowed(self, value): + def multiple_selection_allowed(self, value): if value: self.__dict__["multiple"] = "multiple" else: @@ -4093,7 +4093,7 @@ def multiple_selection_allowed(self, value): @editor_attribute_decorator("WidgetSpecific", '''Defines the path where to save the file''', str, {}) def savepath(self): return self._savepath @savepath.setter - def savepath(self, value): + def savepath(self, value): self._savepath = value def __init__(self, savepath='./', multiple_selection_allowed=False, accepted_files='*.*', *args, **kwargs): @@ -4245,7 +4245,7 @@ def attr_stroke(self): return self.attributes.get('stroke', None) @attr_stroke.setter def attr_stroke(self, value): self.attributes['stroke'] = str(value) @attr_stroke.deleter - def attr_stroke(self): del self.attributes['stroke'] + def attr_stroke(self): del self.attributes['stroke'] @property @editor_attribute_decorator("WidgetSpecific", '''Stroke width for svg elements.''', float, {'possible_values': '', 'min': 0.0, 'max': 10000.0, 'default': 1.0, 'step': 0.1}) @@ -4253,7 +4253,7 @@ def attr_stroke_width(self): return self.attributes.get('stroke-width', None) @attr_stroke_width.setter def attr_stroke_width(self, value): self.attributes['stroke-width'] = str(value) @attr_stroke_width.deleter - def attr_stroke_width(self): del self.attributes['stroke-width'] + def attr_stroke_width(self): del self.attributes['stroke-width'] def set_stroke(self, width=1, color='black'): """Sets the stroke properties. @@ -4273,7 +4273,7 @@ def css_transform(self): return self.style.get('transform', None) @css_transform.setter def css_transform(self, value): self.style['transform'] = str(value) @css_transform.deleter - def css_transform(self): del self.style['transform'] + def css_transform(self): del self.style['transform'] @property @editor_attribute_decorator("Transformation", '''Transform origin as percent or absolute x,y pair value or ['center','top','bottom','left','right'] .''', str, {}) @@ -4281,7 +4281,7 @@ def css_transform_origin(self): return self.style.get('transform-origin', None) @css_transform_origin.setter def css_transform_origin(self, value): self.style['transform-origin'] = str(value) @css_transform_origin.deleter - def css_transform_origin(self): del self.style['transform-origin'] + def css_transform_origin(self): del self.style['transform-origin'] @property @editor_attribute_decorator("Transformation", '''Alters the behaviour of tranform and tranform-origin by defining the transform box.''', 'DropDown', {'possible_values': ('content-box','border-box','fill-box','stroke-box','view-box')}) @@ -4289,7 +4289,7 @@ def css_transform_box(self): return self.style.get('transform-box', None) @css_transform_box.setter def css_transform_box(self, value): self.style['transform-box'] = str(value) @css_transform_box.deleter - def css_transform_box(self): del self.style['transform-box'] + def css_transform_box(self): del self.style['transform-box'] class _MixinSvgFill(): @@ -4299,7 +4299,7 @@ def attr_fill(self): return self.attributes.get('fill', None) @attr_fill.setter def attr_fill(self, value): self.attributes['fill'] = str(value) @attr_fill.deleter - def attr_fill(self): del self.attributes['fill'] + def attr_fill(self): del self.attributes['fill'] @property @editor_attribute_decorator("WidgetSpecific", '''Fill opacity for svg elements.''', float, {'possible_values': '', 'min': 0.0, 'max': 1.0, 'default': 1.0, 'step': 0.1}) @@ -4307,7 +4307,7 @@ def attr_fill_opacity(self): return self.attributes.get('fill-opacity', None) @attr_fill_opacity.setter def attr_fill_opacity(self, value): self.attributes['fill-opacity'] = str(value) @attr_fill_opacity.deleter - def attr_fill_opacity(self): del self.attributes['fill-opacity'] + def attr_fill_opacity(self): del self.attributes['fill-opacity'] def set_fill(self, color='black'): """Sets the fill color. @@ -4368,7 +4368,7 @@ def set_size(self, w, h): class SvgStop(Tag): """ """ - + @property @editor_attribute_decorator("WidgetSpecific", '''Gradient color''', 'ColorPicker', {}) def css_stop_color(self): return self.style.get('stop-color', None) @@ -4406,7 +4406,7 @@ class SvgGradientLinear(Tag): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_x1(self): return self.attributes.get('x1', None) @attr_x1.setter - def attr_x1(self, value): + def attr_x1(self, value): self.attributes['x1'] = str(value) if not self.attributes['x1'][-1] == '%': self.attributes['x1'] = self.attributes['x1'] + '%' @@ -4415,16 +4415,16 @@ def attr_x1(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_y1(self): return self.attributes.get('y1', None) @attr_y1.setter - def attr_y1(self, value): + def attr_y1(self, value): self.attributes['y1'] = str(value) if not self.attributes['y1'][-1] == '%': self.attributes['y1'] = self.attributes['y1'] + '%' - + @property @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_x2(self): return self.attributes.get('x2', None) @attr_x2.setter - def attr_x2(self, value): + def attr_x2(self, value): self.attributes['x2'] = str(value) if not self.attributes['x2'][-1] == '%': self.attributes['x2'] = self.attributes['x2'] + '%' @@ -4433,7 +4433,7 @@ def attr_x2(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_y2(self): return self.attributes.get('y2', None) @attr_y2.setter - def attr_y2(self, value): + def attr_y2(self, value): self.attributes['y2'] = str(value) if not self.attributes['y2'][-1] == '%': self.attributes['y2'] = self.attributes['y2'] + '%' @@ -4453,7 +4453,7 @@ class SvgGradientRadial(Tag): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_cx(self): return self.attributes.get('cx', None) @attr_cx.setter - def attr_cx(self, value): + def attr_cx(self, value): self.attributes['cx'] = str(value) if not self.attributes['cx'][-1] == '%': self.attributes['cx'] = self.attributes['cx'] + '%' @@ -4462,7 +4462,7 @@ def attr_cx(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_cy(self): return self.attributes.get('cy', None) @attr_cy.setter - def attr_cy(self, value): + def attr_cy(self, value): self.attributes['cy'] = str(value) if not self.attributes['cy'][-1] == '%': self.attributes['cy'] = self.attributes['cy'] + '%' @@ -4471,7 +4471,7 @@ def attr_cy(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_fx(self): return self.attributes.get('fx', None) @attr_fx.setter - def attr_fx(self, value): + def attr_fx(self, value): self.attributes['fx'] = str(value) if not self.attributes['fx'][-1] == '%': self.attributes['fx'] = self.attributes['fx'] + '%' @@ -4480,7 +4480,7 @@ def attr_fx(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient coordinate value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_fy(self): return self.attributes.get('fy', None) @attr_fy.setter - def attr_fy(self, value): + def attr_fy(self, value): self.attributes['fy'] = str(value) if not self.attributes['fy'][-1] == '%': self.attributes['fy'] = self.attributes['fy'] + '%' @@ -4489,7 +4489,7 @@ def attr_fy(self, value): @editor_attribute_decorator("WidgetSpecific", '''Gradient radius value. It is expressed in percentage''', float, {'possible_values': '', 'min': 0, 'max': 100, 'default': 0, 'step': 1}) def attr_r(self): return self.attributes.get('r', None) @attr_r.setter - def attr_r(self, value): + def attr_r(self, value): self.attributes['r'] = str(value) if not self.attributes['r'][-1] == '%': self.attributes['r'] = self.attributes['r'] + '%' @@ -4509,7 +4509,7 @@ class SvgDefs(Tag): def __init__(self, *args, **kwargs): super(SvgDefs, self).__init__(*args, **kwargs) self.type = 'defs' - + class Svg(Container): """svg widget - is a container for graphic widgets such as SvgCircle, SvgLine and so on.""" @@ -4519,7 +4519,7 @@ def attr_preserveAspectRatio(self): return self.attributes.get('preserveAspectRa @attr_preserveAspectRatio.setter def attr_preserveAspectRatio(self, value): self.attributes['preserveAspectRatio'] = str(value) @attr_preserveAspectRatio.deleter - def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] + def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] @property @editor_attribute_decorator("WidgetSpecific",'''viewBox of the svg drawing. es='x, y, width, height' ''', 'str', {}) @@ -4527,7 +4527,7 @@ def attr_viewBox(self): return self.attributes.get('viewBox', None) @attr_viewBox.setter def attr_viewBox(self, value): self.attributes['viewBox'] = str(value) @attr_viewBox.deleter - def attr_viewBox(self): del self.attributes['viewBox'] + def attr_viewBox(self): del self.attributes['viewBox'] def __init__(self, *args, **kwargs): """ @@ -4536,7 +4536,7 @@ def __init__(self, *args, **kwargs): """ super(Svg, self).__init__(*args, **kwargs) self.type = 'svg' - + def set_viewbox(self, x, y, w, h): """Sets the origin and size of the viewbox, describing a virtual view area. @@ -4612,7 +4612,7 @@ def attr_preserveAspectRatio(self): return self.attributes.get('preserveAspectRa @attr_preserveAspectRatio.setter def attr_preserveAspectRatio(self, value): self.attributes['preserveAspectRatio'] = str(value) @attr_preserveAspectRatio.deleter - def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] + def attr_preserveAspectRatio(self): del self.attributes['preserveAspectRatio'] @property @editor_attribute_decorator("WidgetSpecific", '''Image data or url or a base64 data string, html attribute xlink:href''', 'base64_image', {}) @@ -4620,7 +4620,7 @@ def image_data(self): return self.attributes.get('xlink:href', '') @image_data.setter def image_data(self, value): self.attributes['xlink:href'] = str(value) @image_data.deleter - def image_data(self): del self.attributes['xlink:href'] + def image_data(self): del self.attributes['xlink:href'] def __init__(self, image_data='', x=0, y=0, w=100, h=100, *args, **kwargs): """ @@ -4798,7 +4798,7 @@ class SvgPolyline(Widget, _MixinSvgStroke, _MixinSvgFill, _MixinTransformable): @editor_attribute_decorator("WidgetSpecific",'''Defines the maximum values count.''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) def maxlen(self): return self.__maxlen @maxlen.setter - def maxlen(self, value): + def maxlen(self, value): self.__maxlen = int(value) self.coordsX = collections.deque(maxlen=self.__maxlen) self.coordsY = collections.deque(maxlen=self.__maxlen) @@ -4835,7 +4835,7 @@ def attr_textLength(self): return self.attributes.get('textLength', None) @attr_textLength.setter def attr_textLength(self, value): self.attributes['textLength'] = str(value) @attr_textLength.deleter - def attr_textLength(self): del self.attributes['textLength'] + def attr_textLength(self): del self.attributes['textLength'] @property @editor_attribute_decorator("WidgetSpecific", '''Controls how text is stretched to fit the length.''', 'DropDown', {'possible_values': ('spacing','spacingAndGlyphs')}) @@ -4843,7 +4843,7 @@ def attr_lengthAdjust(self): return self.attributes.get('lengthAdjust', None) @attr_lengthAdjust.setter def attr_lengthAdjust(self, value): self.attributes['lengthAdjust'] = str(value) @attr_lengthAdjust.deleter - def attr_lengthAdjust(self): del self.attributes['lengthAdjust'] + def attr_lengthAdjust(self): del self.attributes['lengthAdjust'] @property @editor_attribute_decorator("WidgetSpecific", '''Rotation angle for svg elements.''', float, {'possible_values': '', 'min': -360.0, 'max': 360.0, 'default': 1.0, 'step': 0.1}) @@ -4851,7 +4851,7 @@ def attr_rotate(self): return self.attributes.get('rotate', None) @attr_rotate.setter def attr_rotate(self, value): self.attributes['rotate'] = str(value) @attr_rotate.deleter - def attr_rotate(self): del self.attributes['rotate'] + def attr_rotate(self): del self.attributes['rotate'] @property @editor_attribute_decorator("WidgetSpecific", '''Description.''', 'DropDown', {'possible_values': ('start', 'middle', 'end')}) diff --git a/remi/server.py b/remi/server.py index e7212b4a..3259811c 100644 --- a/remi/server.py +++ b/remi/server.py @@ -208,7 +208,7 @@ def send_message(self, message): if not self.handshake_done: self._log.warning("ignoring message %s (handshake not done)" % message[:10]) return False - + if message[0] == "2": i = 0 @@ -259,8 +259,8 @@ def handshake(self): self.request.sendall(response.encode("utf-8")) self.handshake_done = True - #if an update happens since the websocket connection to its handshake, - # it gets not displayed. it is required to inform App about handshake done, + #if an update happens since the websocket connection to its handshake, + # it gets not displayed. it is required to inform App about handshake done, # to get a full refresh clients[self.session].websocket_handshake_done(self) @@ -392,7 +392,7 @@ def _instance(self): self.update_interval = self.server.update_interval from remi import gui - + head = gui.HEAD(self.server.title) # use the default css, but append a version based on its hash, to stop browser caching head.add_child('internal_css', "\n") @@ -466,7 +466,7 @@ def _idle_loop(self): try: self.do_gui_update() except Exception: - self._log.error('''exception during gui update. It is advisable to + self._log.error('''exception during gui update. It is advisable to use App.update_lock using external threads.''', exc_info=True) def idle(self): @@ -476,7 +476,7 @@ def idle(self): def _need_update(self, emitter=None, child_ignore_update=False): if child_ignore_update: - #the widgets tree is processed to make it available for a intentional + #the widgets tree is processed to make it available for a intentional # client update and to reset the changed flags of changed widget. # Otherwise it will be updated on next update cycle. changed_widget_dict = {} @@ -489,7 +489,7 @@ def _need_update(self, emitter=None, child_ignore_update=False): else: #will be updated after idle loop self._need_update_flag = True - + def do_gui_update(self): """ This method gets called also by Timer, a new thread, and so needs to lock the update """ @@ -518,7 +518,7 @@ def set_root_widget(self, widget): msg = "0" + self.root.identifier + ',' + to_websocket(self._overload(self.page.children['body'].innerHTML({}), filename="internal")) self._send_spontaneous_websocket_message(msg) - + def _send_spontaneous_websocket_message(self, message): for ws in list(self.websockets): # noinspection PyBroadException @@ -537,7 +537,7 @@ def _send_spontaneous_websocket_message(self, message): pass # happens when there are multiple clients else: ws.close(terminate_server=False) - + def execute_javascript(self, code): self._send_spontaneous_websocket_message(_MSG_JS + code) @@ -616,7 +616,7 @@ def do_GET(self): # if this is a ws req, instance a ws handler, add it to App's ws list, return if "Upgrade" in self.headers: if self.headers['Upgrade'].lower() == 'websocket': - #passing arguments to websocket handler, otherwise it will lost the last message, + #passing arguments to websocket handler, otherwise it will lost the last message, # and will be unable to handshake ws = WebSocketsHandler(self.headers, self.request, self.client_address, self.server) return @@ -673,7 +673,7 @@ def _get_static_file(self, filename): if not key in paths: return None return os.path.join(paths[key], path) - + def _overload(self, data, **kwargs): """Used to overload the content before sent back to client""" return data @@ -688,14 +688,14 @@ def _process_all(self, func, **kwargs): self.send_header("Set-Cookie", "remi_session=%s; SameSite=Lax"%(self.session)) self.send_header('Content-type', 'text/html') self.end_headers() - + with self.update_lock: # render the HTML page_content = self.page.repr() self.wfile.write(encode_text("\n")) self.wfile.write(encode_text(self._overload(page_content, filename="internal"))) - + elif static_file: filename = self._get_static_file(static_file.groups()[0]) if not filename: @@ -762,7 +762,7 @@ def onload(self, emitter): def onerror(self, message, source, lineno, colno, error): """ WebPage Event that occurs on webpage errors """ - self._log.debug("""App.onerror event occurred in webpage: + self._log.debug("""App.onerror event occurred in webpage: \nMESSAGE:%s\nSOURCE:%s\nLINENO:%s\nCOLNO:%s\ERROR:%s\n"""%(message, source, lineno, colno, error)) def ononline(self, emitter): From 09b7c3e3b27bae2a6c1f57133626d9c3e6c2aecc Mon Sep 17 00:00:00 2001 From: Vlad Kolesnikov Date: Sat, 7 Jan 2023 06:32:59 +0000 Subject: [PATCH 099/110] Rename dynamic_address to dynamic_web_address --- remi/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/remi/server.py b/remi/server.py index 3259811c..983708ce 100644 --- a/remi/server.py +++ b/remi/server.py @@ -438,7 +438,7 @@ def _instance(self): if hasattr(client, '_update_thread'): self._update_thread = client._update_thread - net_interface_ip = self._net_interface_ip() if not self.server.dynamic_address else '' + net_interface_ip = self._net_interface_ip() if not self.server.dynamic_web_address else '' websocket_timeout_timer_ms = str(self.server.websocket_timeout_timer_ms) pending_messages_queue_length = str(self.server.pending_messages_queue_length) self.page.children['head'].set_internal_js(str(id(self)), net_interface_ip, pending_messages_queue_length, websocket_timeout_timer_ms) @@ -795,7 +795,7 @@ def __init__(self, server_address, RequestHandlerClass, auth, multiple_instance, enable_file_cache, update_interval, websocket_timeout_timer_ms, pending_messages_queue_length, title, server_starter_instance, certfile, keyfile, ssl_version, - dynamic_address, + dynamic_web_address, *userdata): HTTPServer.__init__(self, server_address, RequestHandlerClass) self.auth = auth @@ -806,7 +806,7 @@ def __init__(self, server_address, RequestHandlerClass, self.pending_messages_queue_length = pending_messages_queue_length self.title = title self.server_starter_instance = server_starter_instance - self.dynamic_address = dynamic_address + self.dynamic_web_address = dynamic_web_address self.userdata = userdata self.certfile = certfile @@ -821,7 +821,7 @@ class Server(object): def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=0, username=None, password=None, multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True, websocket_timeout_timer_ms=1000, pending_messages_queue_length=1000, - certfile=None, keyfile=None, ssl_version=None, userdata=(), dynamic_address=False): + certfile=None, keyfile=None, ssl_version=None, userdata=(), dynamic_web_address=False): self._gui = gui_class self._title = title or gui_class.__name__ @@ -840,7 +840,7 @@ def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=0, self._keyfile = keyfile self._ssl_version = ssl_version self._userdata = userdata - self._dynamic_address = dynamic_address + self._dynamic_web_address = dynamic_web_address if username and password: self._auth = base64.b64encode(encode_text("%s:%s" % (username, password))) else: @@ -871,7 +871,7 @@ def start(self): self._multiple_instance, self._enable_file_cache, self._update_interval, self._websocket_timeout_timer_ms, self._pending_messages_queue_length, self._title, - self, self._certfile, self._keyfile, self._ssl_version, self._dynamic_address, + self, self._certfile, self._keyfile, self._ssl_version, self._dynamic_web_address, *self._userdata) shost, sport = self._sserver.socket.getsockname()[:2] self._log.info('Started httpserver http://%s:%s/'%(shost,sport)) From 1f5e802cbccf297571e41c8c4b9e906536bdff7e Mon Sep 17 00:00:00 2001 From: Vlad Kolesnikov Date: Sat, 7 Jan 2023 19:40:59 +0000 Subject: [PATCH 100/110] Using original pathname for the websocket in dynamic address mode --- remi/gui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/remi/gui.py b/remi/gui.py index 5e6d75ec..7a6df94d 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -1406,10 +1406,11 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que else{ host = document.location.host; port = document.location.port; + pathname = document.location.pathname; if (port != ''){ port = `:${port}`; } - wss_url = `${ws_wss}://${document.location.host}${port}/`; + wss_url = `${ws_wss}://${document.location.host}${port}${pathname}`; } this._ws = new WebSocket(wss_url); From d2885f464b1fd72af912f9098791f5fa7153777b Mon Sep 17 00:00:00 2001 From: Vlad Kolesnikov Date: Sat, 7 Jan 2023 22:27:21 +0000 Subject: [PATCH 101/110] document.location.host includes port, so no separate port handling needed --- remi/gui.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 7a6df94d..e185f9b9 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -1405,12 +1405,8 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que } else{ host = document.location.host; - port = document.location.port; pathname = document.location.pathname; - if (port != ''){ - port = `:${port}`; - } - wss_url = `${ws_wss}://${document.location.host}${port}${pathname}`; + wss_url = `${ws_wss}://${document.location.host}${pathname}`; } this._ws = new WebSocket(wss_url); From c6a61daf929ded2400e82082805817ad51539eab Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 1 Feb 2023 22:08:55 +0100 Subject: [PATCH 102/110] Fixed classname in editor subclassing. --- editor/editor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/editor/editor.py b/editor/editor.py index ddb9b8fb..3a0675fd 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -444,7 +444,7 @@ def repr_widget_for_editor(self, widget, first_node=False): widgetVarName = widget.variable_name classname = 'CLASS' + \ - widgetVarName if widget.attr_editor_newclass else widget.__class__.__name__ + widgetVarName if widget.attr_editor_newclass else widget.attr_class #widget.__class__.__name__ code_nested = prototypes.proto_widget_allocation % { 'varname': widgetVarName, 'classname': classname} @@ -530,7 +530,8 @@ def repr_widget_for_editor(self, widget, first_node=False): if widget.attr_editor_newclass: if not widget.identifier in self.code_declared_classes: self.code_declared_classes[widget.identifier] = '' - self.code_declared_classes[widget.identifier] = prototypes.proto_code_class % {'classname': classname, 'superclassname': widget.__class__.__name__, + + self.code_declared_classes[widget.identifier] = prototypes.proto_code_class % {'classname': classname, 'superclassname': widget.attr_class, 'nested_code': children_code_nested} + self.code_declared_classes[widget.identifier] else: code_nested = code_nested + children_code_nested @@ -649,12 +650,12 @@ def export_widget_for_app_template(self, widget, first_node=False): if first_node: if len(events_registration) < 1: events_registration = 'pass' - self.code_declared_classes[widget.identifier] = prototypes.proto_export_app_template % {'classname': classname, 'superclassname': widget.__class__.__name__, + self.code_declared_classes[widget.identifier] = prototypes.proto_export_app_template % {'classname': classname, 'superclassname': widget.attr_class, 'nested_code': code_nested + children_code_nested, 'events_registration': events_registration} + self.code_declared_classes[widget.identifier] code_nested = '' else: children_code_nested += events_registration - self.code_declared_classes[widget.identifier] = prototypes.proto_code_class % {'classname': classname, 'superclassname': widget.__class__.__name__, + self.code_declared_classes[widget.identifier] = prototypes.proto_code_class % {'classname': classname, 'superclassname': widget.attr_class, 'nested_code': children_code_nested} + self.code_declared_classes[widget.identifier] else: code_nested = code_nested + children_code_nested From 6c9bc9f5eaad2dfead492cad2362400623d1adb4 Mon Sep 17 00:00:00 2001 From: lopatoid Date: Tue, 21 Feb 2023 07:48:57 +0400 Subject: [PATCH 103/110] make download function work with non-US-ASCII characters --- remi/gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index f363b849..289823ba 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -44,7 +44,7 @@ unescape = html.unescape from .server import runtimeInstances - +import urllib.parse log = logging.getLogger('remi.gui') @@ -4136,8 +4136,9 @@ def __init__(self, text, filename, path_separator='/', *args, **kwargs): def download(self): with open(self._filename, 'r+b') as f: content = f.read() + filename = urllib.parse.quote(os.path.basename(self._filename)) headers = {'Content-type': 'application/octet-stream', - 'Content-Disposition': 'attachment; filename="%s"' % os.path.basename(self._filename)} + 'Content-Disposition': f'attachment; filename="{filename}"; filename*=UTF-8\'\'{filename}'} return [content, headers] From d7522b1dd9bd64558ed70b17c90b96f9a2414ff9 Mon Sep 17 00:00:00 2001 From: lopatoid Date: Tue, 21 Feb 2023 18:32:06 +0400 Subject: [PATCH 104/110] commit 6c9bc9f, but compatible with Python 2 --- remi/gui.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 289823ba..b687ddc3 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -44,7 +44,10 @@ unescape = html.unescape from .server import runtimeInstances -import urllib.parse +try: + from urllib.parse import quote +except ImportError: + from urllib import quote log = logging.getLogger('remi.gui') @@ -4136,9 +4139,9 @@ def __init__(self, text, filename, path_separator='/', *args, **kwargs): def download(self): with open(self._filename, 'r+b') as f: content = f.read() - filename = urllib.parse.quote(os.path.basename(self._filename)) + filename = quote(os.path.basename(self._filename)) headers = {'Content-type': 'application/octet-stream', - 'Content-Disposition': f'attachment; filename="{filename}"; filename*=UTF-8\'\'{filename}'} + 'Content-Disposition': 'attachment; filename="{0}"; filename*=UTF-8\'\'{0}'.format(filename)} return [content, headers] From a1eb2e398c5c1fc22b10bd0a76e64a20a3056f9e Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 22 Mar 2023 21:24:39 +0100 Subject: [PATCH 105/110] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea328b04..460702c6 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ class MyApp(App): self.bt.set_text('Hi!') # starts the web server -start(MyApp) +start(MyApp, port=8081) ``` In order to see the user interface, open your preferred browser and type "http://127.0.0.1:8081". From ce70eb05738f786bd4b9d6583f9e89b815b88bfd Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Wed, 22 Mar 2023 21:25:49 +0100 Subject: [PATCH 106/110] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 460702c6..699cc3ed 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Outside the main class, start the application by calling the function `start` an ```py # starts the webserver -start(MyApp) +start(MyApp, port=8081) ``` Run the script. If it's all OK the GUI will be opened automatically in your browser, otherwise, you have to type in the address bar "http://127.0.0.1:8081". From 4be71eb0ab15ade4a6e327a74ad329838e8ec022 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Mon, 26 Jun 2023 13:09:35 +0200 Subject: [PATCH 107/110] Do not require write permission when downloading files (#518) --- remi/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remi/gui.py b/remi/gui.py index f363b849..7c5e9da3 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -4134,7 +4134,7 @@ def __init__(self, text, filename, path_separator='/', *args, **kwargs): self._path_separator = path_separator def download(self): - with open(self._filename, 'r+b') as f: + with open(self._filename, 'rb') as f: content = f.read() headers = {'Content-type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="%s"' % os.path.basename(self._filename)} From 9a90a96e96ca0a2d89b846e5f2d626092cea3a8d Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Mon, 27 Jan 2025 22:51:19 +0100 Subject: [PATCH 108/110] FileUploader.onprogress event. Issue #534 --- examples/progress_bar_app.py | 50 ++++++++++++++++++++++++++++++++++++ remi/gui.py | 24 +++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 examples/progress_bar_app.py diff --git a/examples/progress_bar_app.py b/examples/progress_bar_app.py new file mode 100644 index 00000000..674b9624 --- /dev/null +++ b/examples/progress_bar_app.py @@ -0,0 +1,50 @@ +""" + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import remi.gui as gui +from remi import start, App + + +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'}) + + # creating a progress bar and appending it to the main layout + self.progress = gui.Progress(0,100) + main_container.append(self.progress) + + # creating the file uploader + self.file_uploader = gui.FileUploader() + main_container.append(self.file_uploader) + + # linking events + self.file_uploader.onprogress.do(self.onprogress_listener) + self.file_uploader.onsuccess.do(self.fileupload_on_success) + + # returning the root widget + return main_container + + def onprogress_listener(self, emitter, filename, loaded, total): + self.progress.set_value(loaded*100.0/total) + print(filename, loaded, total) + + def fileupload_on_success(self, emitter, filename): + print('File upload success: ' + filename) + + +if __name__ == "__main__": + # starts the webserver + start(MyApp, address='0.0.0.0', port=0, start_browser=True, username=None, password=None) diff --git a/remi/gui.py b/remi/gui.py index 7c5e9da3..ea7e1962 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -1582,6 +1582,18 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que Remi.prototype.uploadFile = function(widgetID, eventSuccess, eventFail, eventData, file){ var url = '/'; var xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('progress', function(e) { + console.log('progress!', widgetID, %(emitter_identifier)s, e.loaded, e.total); + if(event.lengthComputable){ + var params={}; + params['filename'] = 'filename'/* file.name*/; + params['loaded'] = event.loaded; + params['total'] = event.total; + console.log("length is computable; sending callback"); + remi.sendCallbackParam(widgetID,'onprogress',params); + } + }); + var fd = new FormData(); xhr.open('POST', url, true); xhr.setRequestHeader('filename', file.name); @@ -1599,6 +1611,7 @@ def set_internal_js(self, app_identifier, net_interface_ip, pending_messages_que console.log('upload failed: ' + file.name); } }; + fd.append('upload_file', file); xhr.send(fd); }; @@ -4120,6 +4133,17 @@ def ondata(self, filedata, filename): f.write(filedata) return (filedata, filename) + @decorate_set_on_listener("(self, emitter, filename, loaded, total)") + @decorate_event + def onprogress(self, filename, loaded, total): + """ + Args: + filename (str): the file name that is uploading + loaded (int): loaded bytes + total (int): file size in bytes + """ + return (filename, int(loaded), int(total)) + class FileDownloader(Container, _MixinTextualWidget): """FileDownloader widget. Allows to start a file download.""" From a4aaa61982c3384eda4e0b1188517771ca89e3d3 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Sun, 9 Mar 2025 19:42:37 +0100 Subject: [PATCH 109/110] Additional functionality #539. --- remi/gui.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index ea7e1962..5707cd10 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -2255,23 +2255,11 @@ def __init__(self, single_line=True, hint='', *args, **kwargs): self.type = 'textarea' self.single_line = single_line + self.attributes['single_line'] = 'false' if single_line: self.style['resize'] = 'none' self.attributes['rows'] = '1' - self.attributes[self.EVENT_ONINPUT] = """ - var elem = document.getElementById('%(emitter_identifier)s'); - var enter_pressed = (elem.value.indexOf('\\n') > -1); - if(enter_pressed){ - elem.value = elem.value.split('\\n').join(''); - var params={};params['new_value']=elem.value; - remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); - }""" % {'emitter_identifier': str(self.identifier), 'event_name': Widget.EVENT_ONCHANGE} - #else: - # self.attributes[self.EVENT_ONINPUT] = """ - # var elem = document.getElementById('%(emitter_identifier)s'); - # var params={};params['new_value']=elem.value; - # remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); - # """ % {'emitter_identifier': str(self.identifier), 'event_name': Widget.EVENT_ONCHANGE} + self.attributes['single_line'] = 'true' self.set_value('') @@ -2303,7 +2291,29 @@ def get_value(self): return self.get_text() @decorate_set_on_listener("(self, emitter, new_value)") - @decorate_event + @decorate_event_js(""" + var is_single_line = (parseInt(document.getElementById('%(emitter_identifier)s').getAttribute('rows')) < 2); + var elem = document.getElementById('%(emitter_identifier)s'); + var enter_pressed = (elem.value.indexOf('\\n') > -1); + if(enter_pressed && is_single_line){ + elem.value = elem.value.split('\\n').join(''); + } + var params={}; + params['new_value']=elem.value; + params['enter_pressed']=enter_pressed; + remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params); + """) + def oninput(self, new_value, enter_pressed): + enter_pressed = enter_pressed in ('True', 'true') + if self.single_line and enter_pressed: + self.onchange(new_value) + return (new_value,) + + @decorate_set_on_listener("(self, emitter, new_value)") + @decorate_event_js(""" + var params={}; + params['new_value']=document.getElementById('%(emitter_identifier)s').value; + remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);""") def onchange(self, new_value): """Called when the user changes the TextInput content. With single_line=True it fires in case of focus lost and Enter key pressed. From ba6bc5399cb915429743788201778ee0454bf221 Mon Sep 17 00:00:00 2001 From: Davide Rosa Date: Sun, 9 Mar 2025 20:00:36 +0100 Subject: [PATCH 110/110] Removed oninput event from Slider class. --- remi/gui.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/remi/gui.py b/remi/gui.py index 5707cd10..72be7f51 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -3508,11 +3508,6 @@ def __init__(self, default_value=0, min=0, max=65535, step=1, **kwargs): "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);"% \ {'emitter_identifier':str(self.identifier), 'event_name':Widget.EVENT_ONCHANGE} - @decorate_set_on_listener("(self, emitter, value)") - @decorate_event - def oninput(self, value): - return (value, ) - class ColorPicker(Input):