diff --git a/examples/optioncontainer/README.rst b/examples/optioncontainer/README.rst new file mode 100644 index 0000000000..04b0f5769f --- /dev/null +++ b/examples/optioncontainer/README.rst @@ -0,0 +1,12 @@ +Option Container Example +======================== + +Test app for the Option Container Example widget. + +Quickstart +~~~~~~~~~~ + +To run this example: + + $ pip install toga + $ python -m optioncontainer diff --git a/examples/optioncontainer/optioncontainer/__init__.py b/examples/optioncontainer/optioncontainer/__init__.py new file mode 100644 index 0000000000..5de825baaa --- /dev/null +++ b/examples/optioncontainer/optioncontainer/__init__.py @@ -0,0 +1,9 @@ +# Examples of valid version strings +# __version__ = '1.2.3.dev1' # Development release 1 +# __version__ = '1.2.3a1' # Alpha Release 1 +# __version__ = '1.2.3b1' # Beta Release 1 +# __version__ = '1.2.3rc1' # RC Release 1 +# __version__ = '1.2.3' # Final Release +# __version__ = '1.2.3.post1' # Post Release 1 + +__version__ = '0.0.1' diff --git a/examples/optioncontainer/optioncontainer/__main__.py b/examples/optioncontainer/optioncontainer/__main__.py new file mode 100644 index 0000000000..5877b9b83e --- /dev/null +++ b/examples/optioncontainer/optioncontainer/__main__.py @@ -0,0 +1,4 @@ +from optioncontainer.app import main + +if __name__ == '__main__': + main().main_loop() diff --git a/examples/optioncontainer/optioncontainer/app.py b/examples/optioncontainer/optioncontainer/app.py new file mode 100644 index 0000000000..b55f03acf6 --- /dev/null +++ b/examples/optioncontainer/optioncontainer/app.py @@ -0,0 +1,140 @@ +import toga +from toga.style import Pack +from toga.constants import COLUMN, ROW + + +class ExampleOptionContainerApp(toga.App): + + def _create_options(self): + label_box0 = toga.Label('This is Box 0 ', style=Pack(padding=10)) + label_box1 = toga.Label('This is Box 1 ', style=Pack(padding=10)) + label_box2 = toga.Label('This is Box 2 ', style=Pack(padding=10)) + + box0 = toga.Box(children=[label_box0]) + box1 = toga.Box(children=[label_box1]) + box2 = toga.Box(children=[label_box2]) + + self.optioncontainer.add('Option 0', box0) + self.optioncontainer.add('Option 1', box1) + self.optioncontainer.add('Option 2', box2) + self._refresh_select() + + def _refresh_select(self): + items = [] + for i in range(len(self.optioncontainer.content)): + items.append(str(i)) + self.select_option.items = items + + def on_add_option(self, button): + self.optioncontainer.add('New Option', toga.Box()) + self._refresh_select() + + def on_enable_option(self, button): + index = int(self.select_option.value) + try: + self.optioncontainer.content[index].enabled = not self.optioncontainer.content[index].enabled + except toga.OptionContainer.OptionException as e: + self.main_window.info_dialog('Oops', str(e)) + + def on_change_option_title(self, button): + index = int(self.select_option.value) + self.optioncontainer.content[index].label = self.input_change_title.value + + def on_remove_option(self, button): + try: + index = int(self.select_option.value) + del self.optioncontainer.content[index] + self._refresh_select() + except toga.OptionContainer.OptionException as e: + self.main_window.info_dialog('Oops', str(e)) + + def startup(self): + # Set up main window + self.main_window = toga.MainWindow(title=self.name) + + # styles + style_flex = Pack(flex=1, padding=5) + style_row = Pack(direction=ROW, flex=1) + style_select = Pack(direction=ROW, flex=1, padding_right=10) + style_col = Pack(direction=COLUMN, flex=1) + + # select + label_select = toga.Label('Select an Option position:', style=style_flex) + self.select_option = toga.Selection(style=style_flex) + # buttons + btn_remove = toga.Button( + 'Remove', + on_press=self.on_remove_option, + style=style_flex + ) + btn_enabled = toga.Button( + 'Toggle enabled', + on_press=self.on_enable_option, + style=style_flex + ) + # change label + self.input_change_title = toga.TextInput(style=style_flex) + btn_change_title = toga.Button( + 'Change title', + on_press=self.on_change_option_title, + style=style_flex + ) + + box_select = toga.Box( + style=style_select, + children=[label_select, self.select_option] + ) + box_actions_col1 = toga.Box( + style=style_row, + children=[btn_remove, btn_enabled] + ) + box_actions_col2 = toga.Box( + style=style_row, + children=[self.input_change_title, btn_change_title] + ) + box_actions = toga.Box( + style=style_col, + children=[box_actions_col1, box_actions_col2] + ) + box_container_actions = toga.Box( + style=style_row, + children=[box_select, box_actions] + ) + + self.optioncontainer = toga.OptionContainer(style=Pack(padding_bottom=20)) + self._create_options() + + btn_add = toga.Button('Add Option', on_press=self.on_add_option) + box_general_actions = toga.Box( + style=Pack(padding_bottom=10), + children=[btn_add] + ) + + # Outermost box + outer_box = toga.Box( + children=[ + box_general_actions, + box_container_actions, + self.optioncontainer + ], + style=Pack( + flex=1, + direction=COLUMN, + padding=10, + ) + ) + + # Add the content on the main window + self.main_window.content = outer_box + + # Show the main window + self.main_window.show() + + +def main(): + return ExampleOptionContainerApp('Option Container Example', 'org.beeware.widgets.optioncontainer') + + +if __name__ == '__main__': + app = main() + app.main_loop() diff --git a/examples/optioncontainer/optioncontainer/resources/README b/examples/optioncontainer/optioncontainer/resources/README new file mode 100644 index 0000000000..246d0829ed --- /dev/null +++ b/examples/optioncontainer/optioncontainer/resources/README @@ -0,0 +1 @@ +Put any icons or images in this directory. \ No newline at end of file diff --git a/examples/optioncontainer/pyproject.toml b/examples/optioncontainer/pyproject.toml new file mode 100644 index 0000000000..3d7045a30b --- /dev/null +++ b/examples/optioncontainer/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["briefcase"] + +[tool.briefcase] +project_name = "Option Container Example" +bundle = "org.beeware" +version = "0.3.0.dev20" +url = "https://beeware.org" +license = "BSD license" +author = 'Tiberius Yak' +author_email = "tiberius@beeware.org" + +[tool.briefcase.app.optioncontainer] +formal_name = "Option Container Example" +description = "A testing app" +sources = ['optioncontainer'] +requires = [] + + +[tool.briefcase.app.optioncontainer.macOS] +requires = [ + 'toga-cocoa', +] + +[tool.briefcase.app.optioncontainer.linux] +requires = [ + 'toga-gtk', +] + +[tool.briefcase.app.optioncontainer.windows] +requires = [ + 'toga-winforms', +] + +# Mobile deployments +[tool.briefcase.app.optioncontainer.iOS] +requires = [ + 'toga-iOS', +] + +[tool.briefcase.app.optioncontainer.android] +requires = [ + 'toga-android', +] diff --git a/src/cocoa/toga_cocoa/widgets/optioncontainer.py b/src/cocoa/toga_cocoa/widgets/optioncontainer.py index bca24a8f93..1d3ad5a16c 100644 --- a/src/cocoa/toga_cocoa/widgets/optioncontainer.py +++ b/src/cocoa/toga_cocoa/widgets/optioncontainer.py @@ -1,6 +1,11 @@ from rubicon.objc import at -from toga_cocoa.libs import * +from toga_cocoa.libs import ( + NSObject, + objc_method, + NSTabView, + NSTabViewItem +) from toga_cocoa.window import CocoaViewport from .base import Widget @@ -48,5 +53,37 @@ def add_content(self, label, widget): item.view = widget.native self.native.addTabViewItem(item) + def remove_content(self, index): + tabview = self.native.tabViewItemAtIndex(index) + if tabview == self.native.selectedTabViewItem: + # Don't allow removal of a selected tab + raise self.interface.OptionException( + 'Currently selected option cannot be removed' + ) + + self.native.removeTabViewItem(tabview) + def set_on_select(self, handler): pass + + def set_option_enabled(self, index, enabled): + tabview = self.native.tabViewItemAtIndex(index) + if not enabled and tabview == self.native.selectedTabViewItem: + # Don't allow disable a selected tab + raise self.interface.OptionException( + 'Currently selected option cannot be disabled' + ) + + tabview._setTabEnabled(enabled) + + def is_option_enabled(self, index): + tabview = self.native.tabViewItemAtIndex(index) + return tabview._isTabEnabled() + + def set_option_label(self, index, value): + tabview = self.native.tabViewItemAtIndex(index) + tabview.label = value + + def get_option_label(self, index): + tabview = self.native.tabViewItemAtIndex(index) + return tabview.label diff --git a/src/cocoa/toga_cocoa/widgets/selection.py b/src/cocoa/toga_cocoa/widgets/selection.py index 1feed0732b..3e6b96e099 100644 --- a/src/cocoa/toga_cocoa/widgets/selection.py +++ b/src/cocoa/toga_cocoa/widgets/selection.py @@ -38,7 +38,7 @@ def select_item(self, item): self.native.selectItemWithTitle(item) def get_selected_item(self): - return self.native.titleOfSelectedItem + return str(self.native.titleOfSelectedItem) def set_on_select(self, handler): pass diff --git a/src/core/toga/widgets/optioncontainer.py b/src/core/toga/widgets/optioncontainer.py index 4e626ffdbe..d80cfc4d89 100644 --- a/src/core/toga/widgets/optioncontainer.py +++ b/src/core/toga/widgets/optioncontainer.py @@ -3,6 +3,100 @@ from .base import Widget +class OptionItem: + def __init__(self, widget): + self._interface = None + self._index = None + self._widget = widget + + @property + def interface(self): + return self._interface + + @interface.setter + def interface(self, interface): + self._interface = interface + self.refresh() + + @property + def index(self): + return self._index + + @index.setter + def index(self, index): + self._index = index + + @property + def enabled(self): + return self._interface._impl.is_option_enabled(self.index) + + @enabled.setter + def enabled(self, enabled): + self._interface._impl.set_option_enabled(self.index, enabled) + + @property + def label(self): + return self._interface._impl.get_option_label(self.index) + + @label.setter + def label(self, value): + self._interface._impl.set_option_label(self.index, value) + + def refresh(self): + self._widget.refresh() + + +class OptionList: + def __init__(self, interface): + self.interface = interface + self._options = [] + + def __repr__(self): + repr_optionlist = '{}([{}])' + repr_items = ', '.join([ + '{}(title={})'.format( + option.__class__.__name__, + option.label) + for option in self + ]) + return repr_optionlist.format(self.__class__.__name__, repr_items) + + def __setitem__(self, index, option): + self._options[index] = option + + def __getitem__(self, index): + # sync options index attr + self._options[index].index = index + return self._options[index] + + def __delitem__(self, index): + self.interface._impl.remove_content(index) + del self._options[index] + + def __iter__(self): + for i, option in enumerate(self._options): + # sync options index attr + option.index = i + return iter(self._options) + + def __len__(self): + return len(self._options) + + def append(self, label, widget, enabled=True): + self._insert(len(self), label, widget, enabled) + + def insert(self, index, label, widget, enabled=True): + self._insert(index, label, widget, enabled) + + def _insert(self, index, label, widget, enabled=True): + self.interface._impl.add_content(label, widget._impl) + option = OptionItem(widget) + self._options.insert(index, option) + self[index].interface = self.interface + self[index].label = label + self[index].enabled = enabled + + class OptionContainer(Widget): """ The option container widget. @@ -14,6 +108,8 @@ class OptionContainer(Widget): Each tuple in the list is composed of a title for the option and the widget tree that is displayed in the option. """ + class OptionException(ValueError): + pass def __init__(self, id=None, style=None, content=None, on_select=None, factory=None): super().__init__(id=id, style=style, factory=factory) @@ -21,7 +117,7 @@ def __init__(self, id=None, style=None, content=None, on_select=None, factory=No self._impl = self.factory.OptionContainer(interface=self) self.on_select = on_select - self._content = [] + self._content = OptionList(self) if content: for label, widget in content: self.add(label, widget) @@ -33,7 +129,7 @@ def content(self): """ The sub layouts of the `SplitContainer`. Returns: - A ``list`` of :class:`toga.Widget`. Each element of the list + A OptionList ``list`` of :class:`toga.OptionItem`. Each element of the list is a sub layout of the `SplitContainer` Raises: @@ -56,9 +152,10 @@ def add(self, label, widget): widget.app = self.app widget.window = self.window - self._content.append(widget) - self._impl.add_content(label, widget._impl) - widget.refresh() + self._content.append(label, widget) + + def remove(self, index): + del self._content[index] def refresh_sublayouts(self): """Refresh the layout and appearance of this widget.""" diff --git a/src/core/toga/widgets/selection.py b/src/core/toga/widgets/selection.py index 90b46d3935..243a717355 100644 --- a/src/core/toga/widgets/selection.py +++ b/src/core/toga/widgets/selection.py @@ -43,12 +43,11 @@ def items(self): @items.setter def items(self, items): self._impl.remove_all_items() + self._items = items for i in items: self._impl.add_item(i) - self._items = items - @property def value(self): """ The value of the currently selected item. diff --git a/src/dummy/toga_dummy/widgets/optioncontainer.py b/src/dummy/toga_dummy/widgets/optioncontainer.py index b558eaf788..d63a6acfcd 100644 --- a/src/dummy/toga_dummy/widgets/optioncontainer.py +++ b/src/dummy/toga_dummy/widgets/optioncontainer.py @@ -8,5 +8,20 @@ def create(self): def add_content(self, label, widget): self._action('add content', label=label, widget=widget) + def remove_content(self, index): + self._action('remove content', index=index) + def set_on_select(self, handler): self._set_value('on_select', handler) + + def set_option_enabled(self, index, enabled): + self._action('set option enabled', index=index) + + def is_option_enabled(self, index): + self._action('is enabled', index=index) + + def set_option_label(self, index, value): + self._action('set option label', index=index, value=value) + + def get_option_label(self, index): + self._action('get label', index=index) diff --git a/src/gtk/toga_gtk/widgets/optioncontainer.py b/src/gtk/toga_gtk/widgets/optioncontainer.py index f39042da23..4100aef40c 100644 --- a/src/gtk/toga_gtk/widgets/optioncontainer.py +++ b/src/gtk/toga_gtk/widgets/optioncontainer.py @@ -21,3 +21,18 @@ def add_content(self, label, widget): def set_on_select(self, handler): # No special handling required pass + + def remove_content(self, index): + self.interface.factory.not_implemented('OptionContainer.remove_content()') + + def set_option_enabled(self, index, enabled): + self.interface.factory.not_implemented('OptionContainer.set_option_enabled()') + + def is_option_enabled(self, index): + self.interface.factory.not_implemented('OptionContainer.is_option_enabled()') + + def set_option_label(self, index, value): + self.interface.factory.not_implemented('OptionContainer.set_option_label()') + + def get_option_label(self, index): + self.interface.factory.not_implemented('OptionContainer.get_option_label()') diff --git a/src/gtk/toga_gtk/widgets/selection.py b/src/gtk/toga_gtk/widgets/selection.py index ce199f1db2..d7ba56146d 100644 --- a/src/gtk/toga_gtk/widgets/selection.py +++ b/src/gtk/toga_gtk/widgets/selection.py @@ -17,11 +17,9 @@ def gtk_on_select(self, widget): self.interface.on_select(widget) def remove_all_items(self): - # self._text.clear() self.native.remove_all() def add_item(self, item): - # self._text.append(item) self.native.append_text(item) # Gtk.ComboBox does not select the first item, so it's done here. diff --git a/src/winforms/toga_winforms/widgets/optioncontainer.py b/src/winforms/toga_winforms/widgets/optioncontainer.py index 699987db53..88378fb5cc 100644 --- a/src/winforms/toga_winforms/widgets/optioncontainer.py +++ b/src/winforms/toga_winforms/widgets/optioncontainer.py @@ -25,5 +25,20 @@ def add_content(self, label, widget): item.Controls.Add(widget.native) self.native.Controls.Add(item) + def remove_content(self, index): + self.interface.factory.not_implemented('OptionContainer.remove_content()') + def set_on_select(self, handler): self.interface.factory.not_implemented('OptionContainer.set_on_select()') + + def set_option_enabled(self, index, value): + self.interface.factory.not_implemented('OptionContainer.is_option_enabled()') + + def is_option_enabled(self, index): + self.interface.factory.not_implemented('OptionContainer.is_option_enabled()') + + def set_option_label(self, index, value): + self.interface.factory.not_implemented('OptionContainer.set_option_label()') + + def get_option_label(self, index): + self.interface.factory.not_implemented('OptionContainer.get_option_label()')