diff --git a/examples/widgets_overview_app.py b/examples/widgets_overview_app.py index a5df611a..c8d1cc62 100644 --- a/examples/widgets_overview_app.py +++ b/examples/widgets_overview_app.py @@ -1,3 +1,4 @@ +#!/usr/bin/python """ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,9 +30,11 @@ def main(self): # the margin 0px auto centers the main container verticalContainer = gui.Widget(width=540, margin='0px auto', style={'display': 'block', 'overflow': 'hidden'}) - horizontalContainer = gui.Widget(width='100%', layout_orientation=gui.Widget.LAYOUT_HORIZONTAL, margin='0px', style={'display': 'block', 'overflow': 'auto'}) - - subContainerLeft = gui.Widget(width=320, style={'display': 'block', 'overflow': 'auto', 'text-align': 'center'}) + horizontalContainer = gui.Widget(width='100%', layout_orientation=gui.Widget.LAYOUT_HORIZONTAL, margin='0px', + style={'display': 'block', 'overflow': 'auto'}) + + self.subContainerLeft = subContainerLeft = gui.Widget(width=320, + style={'display': 'block', 'overflow': 'auto', 'text-align': 'center'}) self.img = gui.Image('/res:logo.png', height=100, margin='10px') self.img.onclick.do(self.on_img_clicked) @@ -44,7 +47,7 @@ def main(self): self.table.on_table_row_click.do(self.on_table_row_click) # the arguments are width - height - layoutOrientationOrizontal - subContainerRight = gui.Widget(style={'width': '220px', 'display': 'block', 'overflow': 'auto', 'text-align': 'center'}) + self.subContainerRight = subContainerRight = gui.Widget(style={'width': '220px', 'display': 'block', 'overflow': 'auto', 'text-align': 'center'}) self.count = 0 self.counter = gui.Label('', width=200, height=30, margin='10px') @@ -96,13 +99,16 @@ def main(self): self.date = gui.Date('2015-04-13', width=200, height=20, margin='10px') self.date.onchange.do(self.date_changed) + self.radioGroup = gui.RadioHGroup(['radio1', 'radio2'], 0, width=200, height=20, margin='10px') + self.radioGroup.onchange.do(self.on_radio_change) + self.video = gui.Widget( _type='iframe', width=290, height=200, margin='10px') self.video.attributes['src'] = "https://drive.google.com/file/d/0B0J9Lq_MRyn4UFRsblR3UTBZRHc/preview" self.video.attributes['width'] = '100%' self.video.attributes['height'] = '100%' self.video.attributes['controls'] = 'true' self.video.style['border'] = 'none' - + self.tree = gui.TreeView(width='100%', height=300) ti1 = gui.TreeItem("Item1") ti2 = gui.TreeItem("Item2") @@ -117,9 +123,10 @@ def main(self): self.tree.append([ti1, ti2, ti3]) ti2.append([subti1, subti2, subti3, subti4]) subti4.append([subsubti1, subsubti2, subsubti3]) - + # appending a widget to another, the first argument is a string key - subContainerRight.append([self.counter, self.lbl, self.bt, self.txt, self.spin, self.progress, self.check, self.btInputDiag, self.btFileDiag]) + subContainerRight.append([self.counter, self.lbl, self.bt, self.txt, self.spin, self.progress, self.check, + self.radioGroup, self.btInputDiag, self.btFileDiag]) # use a defined key as we replace this widget later fdownloader = gui.FileDownloader('download test', '../remi/res/logo.png', width=200, height=30, margin='10px') subContainerRight.append(fdownloader, key='file_downloader') @@ -130,10 +137,8 @@ def main(self): horizontalContainer.append([subContainerLeft, subContainerRight]) - menu = gui.Menu(width='100%', height='30px') + self.menu = menu = gui.Menu(width='100%', height='30px') m1 = gui.MenuItem('File', width=100, height=30) - m2 = gui.MenuItem('View', width=100, height=30) - m2.onclick.do(self.menu_view_clicked) m11 = gui.MenuItem('Save', width=100, height=30) m12 = gui.MenuItem('Open', width=100, height=30) m12.onclick.do(self.menu_open_clicked) @@ -141,12 +146,32 @@ def main(self): m111.onclick.do(self.menu_save_clicked) m112 = gui.MenuItem('Save as', width=100, height=30) m112.onclick.do(self.menu_saveas_clicked) + m2 = gui.MenuItem('View', width=100, height=30) + m21 = gui.MenuItem('Left panel', width=100, height=30) + m211 = gui.MenuItem('Disable', width=100, height=30) + m211.onclick.do(self.menu_disable_left_panel_clicked) + m212 = gui.MenuItem('Hide', width=100, height=30) + m212.onclick.do(self.menu_hide_left_panel_clicked) + m22 = gui.MenuItem('Right panel', width=100, height=30) + m221 = gui.MenuItem('Disable', width=100, height=30) + m221.onclick.do(self.menu_disable_right_panel_clicked) + m222 = gui.MenuItem('Hide', width=100, height=30) + m222.onclick.do(self.menu_hide_right_panel_clicked) + m23 = gui.MenuItem('About', width=100, height=30) + m231 = gui.MenuItem('Disable', width=100, height=30) + m231.onclick.do(self.menu_disable_about_clicked) m3 = gui.MenuItem('Dialog', width=100, height=30) m3.onclick.do(self.menu_dialog_clicked) + m4 = gui.MenuItem('About', width=100, height=30) + m4.onclick.do(self.menu_about_clicked) - menu.append([m1, m2, m3]) + menu.append([m1, m2, m3, {'about':m4}]) m1.append([m11, m12]) m11.append([m111, m112]) + m2.append([m21, m22, m23]) + m21.append([m211, m212]) + m22.append([m221, m222]) + m23.append([m231]) menubar = gui.MenuBar(width='100%', height='30px') menubar.append(menu) @@ -247,6 +272,9 @@ def on_spin_change(self, widget, newValue): def on_check_change(self, widget, newValue): self.lbl.set_text('CheckBox changed, new value: ' + str(newValue)) + def on_radio_change(self, widget, value, idx, oldIdx): + self.lbl.set_text('Radio changed, on: radio%i' % (idx+1)) + def open_input_dialog(self, widget): self.inputDialog = gui.InputDialog('Input Dialog', 'Your name?', initial_value='type here', @@ -305,8 +333,36 @@ def menu_saveas_clicked(self, widget): def menu_open_clicked(self, widget): self.lbl.set_text('Menu clicked: Open') - def menu_view_clicked(self, widget): - self.lbl.set_text('Menu clicked: View') + def menu_disable_left_panel_clicked(self, widget): + self.lbl.set_text('Disable left panel') + flag = self.subContainerLeft.is_enabled() + self.subContainerLeft.set_enabled(not flag) + widget.set_text('Enable' if flag else 'Disable') + + def menu_disable_right_panel_clicked(self, widget): + self.lbl.set_text('Disable right panel') + flag = self.subContainerRight.is_enabled() + self.subContainerRight.set_enabled(not flag) + widget.set_text('Enable' if flag else 'Disable') + + def menu_hide_left_panel_clicked(self, widget): + self.lbl.set_text('Hide left panel') + flag = self.subContainerLeft.is_visible() + self.subContainerLeft.set_hidden(flag) + widget.set_text('Show' if flag else 'Hide') + + def menu_hide_right_panel_clicked(self, widget): + self.lbl.set_text('Hide right panel') + flag = self.subContainerRight.is_visible() + self.subContainerRight.set_hidden(flag) + widget.set_text('Show' if flag else 'Hide') + + def menu_disable_about_clicked(self, widget): + menu_about = self.menu.get_child('about') + flag = menu_about.is_enabled() + menu_about.set_enabled(not flag) + widget.set_text('Enable' if flag else 'Disable') + self.lbl.set_text('%s About' % ('Disable'if flag else 'Enable' )) def fileupload_on_success(self, widget, filename): self.lbl.set_text('File upload success: ' + filename) @@ -314,6 +370,9 @@ def fileupload_on_success(self, widget, filename): def fileupload_on_failed(self, widget, filename): self.lbl.set_text('File upload failed: ' + filename) + def menu_about_clicked(self, widget): + self.lbl.set_text('Menu clicked: About') + def on_close(self): """ Overloading App.on_close event to stop the Timer. """ diff --git a/remi/gui.py b/remi/gui.py index 40a78ac6..15afb32c 100644 --- a/remi/gui.py +++ b/remi/gui.py @@ -66,7 +66,7 @@ def jsonize(d): def load_resource(filename): """ Convenient function. Given a local path and filename (not in standard remi resource format), - loads the content and returns a base64 encoded data. + loads the content and returns a base64 encoded data. This method allows to bypass the remi resource file management, accessing directly local disk files. Args: @@ -85,7 +85,7 @@ def load_resource(filename): else: data = str(data, 'utf-8') return "data:%(mime)s;base64,%(data)s"%{'mime':mimetype, 'data':data} - + def to_uri(uri_data): """ Convenient function to encase the resource filename or data in url('') keyword @@ -102,13 +102,13 @@ def to_uri(uri_data): class EventSource(object): 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): _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) @@ -130,7 +130,7 @@ def __init__(self, event_source_instance, event_name, event_method_bound): self.callback = None self.userdata = None self.connect = self.do #for compatibility reasons - + def do(self, callback, *userdata): """ 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 @@ -529,7 +529,6 @@ class Widget(Tag, EventSource): @decorate_constructor_parameter_types([]) def __init__(self, children = None, style = None, *args, **kwargs): - """ Args: children (Widget, or iterable of Widgets): The child to be appended. In case of a dictionary, @@ -555,6 +554,10 @@ def __init__(self, children = None, style = None, *args, **kwargs): self.set_layout_orientation(kwargs.get('layout_orientation', Widget.LAYOUT_VERTICAL)) self.set_size(kwargs.get('width'), kwargs.get('height')) self.set_style(style) + self._saved_display_style = None + self._saved_pointer_events_style = None + self._visible = True + self._enable = True if children: self.append(children) @@ -572,14 +575,60 @@ def set_style(self, style): k, v = s.split(':', 1) self.style[k.strip()] = v.strip() + def set_hidden(self, hided): + """ + Args: + hided (bool): hide - if True, show - if False + """ +# for child in self.children.values(): +# if type(child) not in (type(''), type(u'')): +# child.set_hidden(hided) + if hided: + self._saved_display_style = self.style.get('display', None) + try: + self.style['display'] = 'none' + except KeyError: + pass + else: + if self.style['display'] == 'none': + if self._saved_display_style is None: + del self.style['display'] + else: + self.style['display'] = self._saved_display_style + self._visible = not hided + def set_enabled(self, enabled): - if enabled: + """ + Args: + enabled (bool): enable - if True, disable - if False + """ + for child in self.children.values(): + if type(child) not in (type(''), type(u'')): + child.set_enabled(enabled) + if not enabled: + self.attributes['disabled'] = 'true' + self._saved_pointer_events_style = self.style.get('pointer-events', None) try: - del self.attributes['disabled'] + self.style['pointer-events'] = 'none' except KeyError: pass else: - self.attributes['disabled'] = 'True' + try: + del self.attributes['disabled'] + except KeyError: + pass + if self.style['pointer-events'] == 'none': + if self._saved_pointer_events_style is None: + del self.style['pointer-events'] + else: + self.style['pointer-events'] = self._saved_pointer_events_style + self._enable = enabled + + def is_visible(self): + return self._visible + + def is_enabled(self): + return self._enable def set_size(self, width, height): """Set the widget size. @@ -884,7 +933,7 @@ def onkeyup(self, key, keycode, ctrl, shift, alt): """Called when user types and releases a key. The widget should be able to receive the focus in order to emit the event. Assign a 'tabindex' attribute to make it focusable. - + Args: key (str): the character value keycode (str): the numeric char code @@ -903,7 +952,7 @@ def onkeydown(self, key, keycode, ctrl, shift, alt): """Called when user types and releases a key. The widget should be able to receive the focus in order to emit the event. Assign a 'tabindex' attribute to make it focusable. - + Args: key (str): the character value keycode (str): the numeric char code @@ -913,7 +962,7 @@ def onkeydown(self, 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) @@ -961,7 +1010,7 @@ def set_on_touchstart_listener(self, 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) @@ -1010,7 +1059,7 @@ def __init__(self, title, *args, **kwargs): """ """) - + self._classes = [] self.set_title(title) @@ -1026,7 +1075,7 @@ def set_icon_file(self, filename, rel="icon"): def set_icon_data(self, base64_data, mimetype="image/png", rel="icon"): """ Allows to define an icon for the App - + Args: base64_data (str): base64 encoded image data (ie. "data:image/x-icon;base64,AAABAAEAEBA....") mimetype (str): mimetype of the image ("image/png" or "image/x-icon"...) @@ -1322,7 +1371,7 @@ def __init__(self, *args, **kwargs): loading_widget.set_identifier("loading") self.append(loading_widget) - + @decorate_set_on_listener("(self, emitter)") @decorate_event_js("""sendCallback('%(emitter_identifier)s','%(event_name)s'); event.stopPropagation();event.preventDefault(); @@ -1396,6 +1445,7 @@ def define_grid(self, matrix): Args: matrix (list): list of iterables of strings (lists or something else). Items in the matrix have to correspond to a key for the children. + The key must start with a letter. """ self.style['grid-template-areas'] = ''.join("'%s'"%(' '.join(x)) for x in matrix) @@ -1433,7 +1483,7 @@ def append(self, value, key=''): value.style['position'] = 'static' return key - + def remove_child(self, child): if 'grid-area' in child.style.keys(): del child.style['grid-area'] @@ -1454,7 +1504,7 @@ def set_row_sizes(self, values): values (iterable of int or str): values are treated as percentage. """ self.style['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 @@ -1524,11 +1574,11 @@ def set_from_asciiart(self, asciipattern): i=rows[ri].find("|",i+1) columns[row_max_width] = row_max_width - + row_sizes = [] for r in row_defs.keys(): row_sizes.append(float(rows.count(rows[r]))/float(len(rows))*100.0) - + column_sizes = [] prev_size = 0.0 for c in columns.values(): @@ -1580,7 +1630,7 @@ def append(self, value, key=''): for child in value: keys.append( self.append(child) ) return keys - + key = str(key) if not isinstance(value, Widget): raise ValueError('value should be a Widget (otherwise use add_child(key,other)') @@ -1847,7 +1897,7 @@ def onchange(self, new_value): sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);""") def onkeyup(self, new_value, keycode): """Called when user types and releases a key into the TextInput - + Note: This event can't be registered together with Widget.onchange. Args: @@ -2127,7 +2177,7 @@ class ListView(Widget): """ @decorate_constructor_parameter_types([bool]) - def __init__(self, selectable = True, *args, **kwargs): + def __init__(self, selectable=True, *args, **kwargs): """ Args: kwargs: See Widget.__init__() @@ -2395,7 +2445,6 @@ def get_key(self): def onchange(self, value): """Called when a new DropDownItem gets selected. """ - log.debug('combo box. selected %s' % value) self.select_by_value(value) return (value, ) @@ -2889,6 +2938,124 @@ def get_value(self): return 'checked' in self.attributes +class RadioHGroup(Widget): + + @decorate_constructor_parameter_types([[str], int, [str]]) + def __init__(self, labels, checked=0, user_data=[], **kwargs): + """ + Args: + label [(str)]: + checked (int) or (str): + user_data [(str)]: + kwargs: See Widget.__init__() + """ + super(RadioHGroup, self).__init__(**kwargs) + self.set_layout_orientation(Widget.LAYOUT_HORIZONTAL) + user_data = user_data or [ '%i' % i for i in xrange(len(labels)) ] + self._radiogroup = [ RadioBoxLabel(label, False, data) for label, data in zip(labels, user_data) ] + if type(checked) in (type(''), type(u'')): + checked = int(user_data.index(checked)) + self._radiogroup[checked].set_value(True) + self._idx = checked + self.append(self._radiogroup) + for radiobox in self._radiogroup: + radiobox.onchange.connect(self.onchange) + + @decorate_set_on_listener("(self, emitter, value)") + @decorate_event + def onchange(self, widget, value): + prev_idx = self._idx + self._radiogroup[prev_idx].set_value(False) + self._idx = self._radiogroup.index(widget) + return (value, self._idx, prev_idx) + + @decorate_explicit_alias_for_listener_registration + def set_on_change_listener(self, callback, *userdata): + self.onchange.connect(callback, *userdata) + +# TODO +#class RadioVGroup(RadioHGroup): +# def __init__(self, labels, checked=0, user_data=[], **kwargs): +# """ +# Args: +# label [(str)]: +# checked (int) or (str): +# user_data [(str)]: +# kwargs: See Widget.__init__() +# """ +# super(RadioVGroup, self).__init__(labels, checked, user_data, **kwargs) +# self.set_layout_orientation(Widget.LAYOUT_VERTICAL) + + +class RadioBoxLabel(Widget): + + @decorate_constructor_parameter_types([str, bool, str]) + def __init__(self, label='', checked=False, user_data='', **kwargs): + """ + Args: + label (str): + checked (bool): + user_data (str): + kwargs: See Widget.__init__() + """ + super(RadioBoxLabel, self).__init__(**kwargs) + self.set_layout_orientation(Widget.LAYOUT_HORIZONTAL) + self._radiobox = RadioBox(checked, user_data) + self._label = Label(label) + self.append(self._radiobox, key='radiobox') + self.append(self._label, key='label') + + self.set_value = self._radiobox.set_value + self.get_value = self._radiobox.get_value + + self._radiobox.onchange.connect(self.onchange) + + @decorate_set_on_listener("(self, emitter, value)") + @decorate_event + def onchange(self, widget, value): + return (value, ) + + +class RadioBox(Input): + """check box widget useful as numeric input field implements the onchange event.""" + + @decorate_constructor_parameter_types([bool, str]) + def __init__(self, checked=False, user_data='', **kwargs): + """ + Args: + checked (bool): + user_data (str): + kwargs: See Widget.__init__() + """ + super(RadioBox, self).__init__('radio', user_data, **kwargs) + self.set_value(checked) + self.attributes[Widget.EVENT_ONCHANGE] = \ + "var params={};params['value']=document.getElementById('%(emitter_identifier)s').checked;" \ + "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 onchange(self, value): + value = value in ('True', 'true') + self.set_value(value) + return (self.attributes['value'], ) + + def set_value(self, checked, update_ui=1): + if checked: + self.attributes['checked'] = 'checked' + else: + if 'checked' in self.attributes: + del self.attributes['checked'] + + def get_value(self): + """ + Returns: + bool: + """ + return 'checked' in self.attributes + + class SpinBox(Input): """spin box widget useful as numeric input field implements the onchange event. """ @@ -3284,7 +3451,6 @@ def __init__(self, text, *args, **kwargs): self.set_text(text) def append(self, value, key=''): - return self.sub_container.append(value, key=key) @@ -3588,7 +3754,7 @@ def set_size(self, w, h): class SvgImage(SvgRectangle): - """svg image - a raster image element for svg graphics, + """svg image - a raster image element for svg graphics, this have to be appended into Svg elements.""" @decorate_constructor_parameter_types([str, int, int, int, int]) @@ -3770,4 +3936,3 @@ def set_fill(self, color='black'): color (str): stroke color """ self.attributes['fill'] = color -