diff --git a/artwork/error.svg b/artwork/error.svg new file mode 100644 index 00000000..e731dae0 --- /dev/null +++ b/artwork/error.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/artwork/info.svg b/artwork/info.svg new file mode 100644 index 00000000..fcbb3a5b --- /dev/null +++ b/artwork/info.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + i + + diff --git a/examples/canvas.py b/examples/canvas.py new file mode 100644 index 00000000..e59ff3c9 --- /dev/null +++ b/examples/canvas.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" + 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 os +import remi.gui as gui +from remi.game import Color, Rect +from remi.game.canvas import Canvas +from remi.game.draw import line, lines, circle, rect +from remi import start, App + + +class MyApp(App): + canvas = None + def __init__(self, *args): + super(MyApp, self).__init__(*args) + + def main(self, name='world'): + #margin 0px auto allows to center the app to the screen + container = gui.Widget(width=600, height=600) + self.canvas = Canvas(self, resolution=(600, 400), margin='0px auto') + button = gui.Button('Go!') + button.set_on_click_listener(self.draw) + container.append(self.canvas) + container.append(button) + # returning the root widget + return container + + def draw(self, widget): + line(self.canvas, Color(255, 0, 0), (0, 0), (200, 100)) + lines(self.canvas, Color(0, 0, 255), [(200, 100), + (100, 0), + (150, 400)]) + circle(self.canvas, Color(0, 255, 0), (200, 100), 10, width=1) + circle(self.canvas, Color(255, 0, 0), (300, 150), 10) + rect(self.canvas, Color(255, 255, 0), Rect((10, 10), (30, 30)), width=1) + rect(self.canvas, Color(128, 128, 255), Rect((5, 5), (15, 100))) + +if __name__ == "__main__": + print 'Starting with pid: %s' % os.getpid() + # starts the webserver + # optional parameters + # 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, debug=True) diff --git a/examples/canvas_raster.py b/examples/canvas_raster.py new file mode 100644 index 00000000..0b942d01 --- /dev/null +++ b/examples/canvas_raster.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" + 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 os +import remi.gui as gui +from remi.game import Color, Rect +from remi.game.canvas import Canvas +from remi.game.raster import load_image, draw +from remi import start, App + + +class MyApp(App): + canvas = None + def __init__(self, *args): + super(MyApp, self).__init__(*args) + + def main(self, name='world'): + #margin 0px auto allows to center the app to the screen + container = gui.Widget(width=600, height=600) + self.canvas = Canvas(self, resolution=(600, 400), margin='0px auto') + button = gui.Button('Go!') + button.set_on_click_listener(self.draw) + container.append(self.canvas) + container.append(button) + # returning the root widget + return container + + def draw(self, widget): + image = load_image('example.png') + draw(image, self.canvas, position=(10, 10)) + +if __name__ == "__main__": + print 'Starting with pid: %s' % os.getpid() + # starts the webserver + # optional parameters + # 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, debug=True) diff --git a/examples/dialogs_oeverview_app.py b/examples/dialogs_oeverview_app.py new file mode 100644 index 00000000..a4d02a31 --- /dev/null +++ b/examples/dialogs_oeverview_app.py @@ -0,0 +1,57 @@ +""" + 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.dialogs as dialogs +from remi import start, App + + +class MyApp(App): + def __init__(self, *args): + super(MyApp, self).__init__(*args) + + def main(self): + # the margin 0px auto centers the main container + verticalContainer = gui.Widget( + width=540, margin='0px auto', + style={'display': 'block', 'overflow': 'hidden'}) + + info_bt = gui.Button('Show info dialog', + width=200, height=30, margin='10px') + + error_bt = gui.Button('Show error dialog', + width=200, height=30, margin='10px') + + # setting the listener for the onclick event of the button + info_bt.set_on_click_listener(self.show_info_dialog) + error_bt.set_on_click_listener(self.show_error_dialog) + + verticalContainer.append(info_bt) + verticalContainer.append(error_bt) + + # returning the root widget + return verticalContainer + + def show_info_dialog(self, widget): + dialogs.Info('Some information message', width=300).show(self) + + def show_error_dialog(self, widget): + dialogs.Error('Some error message', width=300).show(self) + +if __name__ == "__main__": + # starts the webserver + # optional parameters + # 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, debug=True, address='0.0.0.0', start_browser=True) diff --git a/remi/dialogs.py b/remi/dialogs.py new file mode 100644 index 00000000..49c85374 --- /dev/null +++ b/remi/dialogs.py @@ -0,0 +1,147 @@ +""" + 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 sys + +from .server import runtimeInstances, update_event +from .gui import decorate_set_on_listener, decorate_constructor_parameter_types +from .gui import Widget, Label, Image, Button + +class _DialogBase(Widget): + """Base information dialog. Information dialog can be used to show some + info to the user. The dialog can be "information", "alert" and "error". + In all cases the dialog is exactly the same but the icon changes. + + The Ok button emits the 'confirm_dialog' event. Register the listener to + it with set_on_confirm_dialog_listener. + """ + + EVENT_ONCONFIRM = 'confirm_dialog' + + @decorate_constructor_parameter_types([str, Widget]) + def __init__(self, title='', content=None, **kwargs): + """ + Args: + title (str): The title of the dialog. + message (str): The message description. + kwargs: See Widget.__init__() + """ + super(_DialogBase, self).__init__(**kwargs) + self.set_layout_orientation(Widget.LAYOUT_VERTICAL) + self.style['display'] = 'block' + self.style['overflow'] = 'auto' + self.style['margin'] = '0px auto' + + if len(title) > 0: + t = Label(title) + t.add_class('DialogTitle') + self.append(t) + + if content is not None: + self.append(content) + + self.container = Widget() + self.container.style['display'] = 'block' + self.container.style['overflow'] = 'auto' + self.container.style['margin'] = '5px' + self.container.set_layout_orientation(Widget.LAYOUT_VERTICAL) + self.conf = Button('Ok') + self.conf.set_size(100, 30) + self.conf.style['margin'] = '3px' + hlay = Widget(height=35) + hlay.style['display'] = 'block' + hlay.style['overflow'] = 'visible' + hlay.append(self.conf) + self.conf.style['float'] = 'right' + + self.append(self.container) + self.append(hlay) + + self.conf.attributes[self.EVENT_ONCLICK] = "sendCallback('%s','%s');" % (self.identifier, self.EVENT_ONCONFIRM) + + self.inputs = {} + + self._base_app_instance = None + self._old_root_widget = None + + def confirm_dialog(self): + """Event generated by the OK button click. + """ + self.hide() + return self.eventManager.propagate(self.EVENT_ONCONFIRM, ()) + + @decorate_set_on_listener("confirm_dialog", "(self,emitter)") + def set_on_confirm_dialog_listener(self, callback, *userdata): + """Registers the listener for the GenericDialog.confirm_dialog event. + + Note: The prototype of the listener have to be like my_on_confirm_dialog(self, widget). + + Args: + callback (function): Callback function pointer. + """ + self.eventManager.register_listener(self.EVENT_ONCONFIRM, callback, *userdata) + + def show(self, base_app_instance): + self._base_app_instance = base_app_instance + self._old_root_widget = self._base_app_instance.root + self._base_app_instance.set_root_widget(self) + + def hide(self): + self._base_app_instance.set_root_widget(self._old_root_widget) + + +class Info(_DialogBase): + """Show a information dialog with a little message and button to accept. + + The Ok button emits the 'confirm_dialog' event. Register the listener to + it with set_on_confirm_dialog_listener. + """ + @decorate_constructor_parameter_types([str, Widget]) + def __init__(self, message='', **kwargs): + super(Info, self).__init__( + title='Information', + content=_make_content_(message, '/res/info.png'), + **kwargs) + + +class Error(_DialogBase): + """Show a error dialog with a little message and button to accept. + + The Ok button emits the 'confirm_dialog' event. Register the listener to + it with set_on_confirm_dialog_listener. + """ + @decorate_constructor_parameter_types([str, Widget]) + def __init__(self, message='', **kwargs): + if len(message) == 0: + message = 'Unknown error' + super(Error, self).__init__( + title='Error', + content=_make_content_(message, '/res/error.png'), + **kwargs) + + +def _make_content_(message, icon_name): + container = Widget() + container.style['display'] = 'block' + container.style['overflow'] = 'auto' + container.style['margin'] = '5px' + container.set_layout_orientation(Widget.LAYOUT_HORIZONTAL) + icon = Image(icon_name) + icon.style['margin'] = '5px' + label = Label(message) + label.style['margin'] = '5px' + container.append(icon) + container.append(label) + return container + diff --git a/remi/game/__init__.py b/remi/game/__init__.py new file mode 100644 index 00000000..40a15e89 --- /dev/null +++ b/remi/game/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" + 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. +""" + +from remi.game.rect import Rect +from remi.game.color import Color diff --git a/remi/game/canvas.py b/remi/game/canvas.py new file mode 100644 index 00000000..9784da95 --- /dev/null +++ b/remi/game/canvas.py @@ -0,0 +1,41 @@ +""" + 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. +""" + +from remi.server import App +from remi.gui import Widget +from remi.gui import decorate_constructor_parameter_types + + +def _pixels_(amount): + if isinstance(amount, int): + return '%spx' % amount + else: + return amount + + +class Canvas(Widget): + @decorate_constructor_parameter_types([tuple, App]) + def __init__(self, app_instance, resolution=(0, 0), **kwargs): + kwargs['width'] = _pixels_(resolution[0]) + kwargs['height'] = _pixels_(resolution[1]) + super(Canvas, self).__init__(**kwargs) + self._app = app_instance + self.type = 'canvas' + + @property + def id(self): + return self.attributes['id'] + + def draw(self, js_code): + self._app.execute_javascript(js_code) diff --git a/remi/game/color.py b/remi/game/color.py new file mode 100644 index 00000000..a63da61d --- /dev/null +++ b/remi/game/color.py @@ -0,0 +1,27 @@ +""" + 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. +""" + +def _hex_(c): + return hex(c)[2:].zfill(2) + +class Color(object): + def __init__(self, r, g, b, a=255): + self.r = r + self.g = g + self.b = b + + def __str__(self): + return '#%s%s%s' % (_hex_(self.r), _hex_(self.g), _hex_(self.b)) + + diff --git a/remi/game/draw.py b/remi/game/draw.py new file mode 100644 index 00000000..657671d5 --- /dev/null +++ b/remi/game/draw.py @@ -0,0 +1,70 @@ +""" + 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. +""" + +from remi.game.canvas import Canvas +from remi.game.color import Color + +def rect(canvas, color, rect, width=0): + canvas.draw('''var canvas = document.getElementById('%s'); +var ctx = canvas.getContext('2d'); +%s +ctx.%s(%s);%s''' % ( + canvas.id, + ('ctx.fillStyle = "%s";' % color) if width == 0 else ( + 'ctx.lineWidth = "%spx"; ctx.strokeStyle = "%s"' % (width, color)), + 'fillRect' if width == 0 else 'rect', + rect, + '' if width == 0 else 'ctx.stroke();' + )) + +def line(canvas, color, start_pos, end_pos, width=1): + canvas.draw('''var canvas = document.getElementById('%s'); +var ctx = canvas.getContext('2d'); +ctx.lineWidth = "%spx"; +ctx.strokeStyle = "%s"; +ctx.beginPath(); +ctx.moveTo(%s, %s); +ctx.lineTo(%s, %s); +ctx.stroke();''' % (canvas.id, width, color, + start_pos[0], start_pos[1], + end_pos[0], end_pos[1])) + +def lines(canvas, color, pointlist, width=1): + start = pointlist[0] + pointlist = pointlist[1:] + js = '''var canvas = document.getElementById('%s'); +var ctx = canvas.getContext('2d'); +ctx.lineWidth = "%spx"; +ctx.strokeStyle = "%s"; +ctx.beginPath(); +ctx.moveTo(%s, %s); +''' % (canvas.id, width, color, start[0], start[1]) + for point in pointlist: + js += 'ctx.lineTo(%s, %s);' % (point[0], point[1]) + js += 'ctx.stroke();' + canvas.draw(js) + +def circle(canvas, color, pos, radius, width=0): + canvas.draw('''var canvas = document.getElementById('%s'); +var ctx = canvas.getContext('2d'); +ctx.lineWidth = "%spx"; +ctx.strokeStyle = "%s";%s +ctx.beginPath(); +ctx.arc(%s, %s, %s, 0, 6.30);%s +ctx.stroke();''' % ( + canvas.id, + width, color, + ('ctx.fillStyle="%s";' % color) if (width == 0) else '', + pos[0], pos[1], radius, + 'ctx.fill();' if (width == 0) else '')) diff --git a/remi/game/raster.py b/remi/game/raster.py new file mode 100644 index 00000000..6289c418 --- /dev/null +++ b/remi/game/raster.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +from PIL import Image + + +class RasterImage(object): + data = '' + width = 0 + height = 0 + + +def load_image(image_file): + image = Image.open(image_file) + data = image.tobytes() + data = [ord(b) for b in data] + result = RasterImage() + result.width = image.width + result.height = image.height + result.data = repr(data) + return result + + +def draw(image, canvas, position): + canvas.draw('''var canvas = document.getElementById('%s'); +var ctx = canvas.getContext('2d'); +var image = ctx.createImageData(%s, %s); +image.data.set(new Uint8ClampedArray(%s)); +ctx.putImageData(image, %s, %s);''' % ( + canvas.id, + image.width, image.height, image.data, + position[0], position[1] + )) diff --git a/remi/game/rect.py b/remi/game/rect.py new file mode 100644 index 00000000..0e2d926c --- /dev/null +++ b/remi/game/rect.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" + 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. +""" + + +class Rect(object): + def __init__(self, position, size): + self.x = position[0] + self.y = position[1] + self.w = size[0] + self.h = size[1] + + def __str__(self): + return '%s, %s, %s, %s' % (self.x, self.y, self.w, self.h) + + @property + def width(self): + return self.w + + @property + def height(self): + return self.h + + @property + def centerx(self): + return self.x + (self.w / 2) + + @property + def centery(self): + return self.y + (self.h / 2) + + @property + def center(self): + return (self.centerx, self.centery) + + @property + def size(self): + return (self.w, self.h) + + @property + def top(self): + return self.y + + @property + def bottom(self): + return self.y + self.h + + @property + def left(self): + return self.x + + @property + def right(self): + return self.x + self.w + + @property + def topleft(self): + return (self.left, self.top) + + @property + def bottomleft(self): + return (self.left, self.bottom) + + @property + def topright(self): + return (self.right, self.top) + + @property + def bottomright(self): + return (self.right, self.bottom) + + @property + def midtop(self): + return (self.centerx, self.top) + + @property + def midleft(self): + return (self.left, self.centery) + + @property + def midbottom(self): + return (self.centerx, self.bottom) + + @property + def midright(self): + return (self.right, self.centery) diff --git a/remi/res/error.png b/remi/res/error.png new file mode 100644 index 00000000..249489d7 Binary files /dev/null and b/remi/res/error.png differ diff --git a/remi/res/info.png b/remi/res/info.png new file mode 100644 index 00000000..c4c8496e Binary files /dev/null and b/remi/res/info.png differ diff --git a/remi/server.py b/remi/server.py index cc215456..d0d91b4b 100644 --- a/remi/server.py +++ b/remi/server.py @@ -695,6 +695,7 @@ def set_root_widget(self, widget): def _send_spontaneous_websocket_message(self, message): global update_lock + self._log.debug('spontaneous message, lock: %s' % update_lock) with update_lock: for ws in self.client.websockets: # noinspection PyBroadException diff --git a/setup.py b/setup.py index cd2985be..440f1840 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author='Davide Rosa', author_email='dddomodossola@gmail.com', license='Apache', - packages=['remi'], + packages=['remi', 'remi.game'], include_package_data=True, zip_safe=False, -) \ No newline at end of file +)