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 @@
+
+
+
+
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 @@
+
+
+
+
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
+)