class PortsTab(Tab,
               HasCommonSetup,
               SubscribesToEvents,
               Resetable):
    """Used to create, edit and remove components.

    Attributes:
        __selected_port: Currently selected port in the ports listview.
        __selected_component: Currently selected component in the components taxonomy view.
    """
    def __init__(self, parent_notebook):
        self.__state = State()

        self.__selected_port: Optional[Port] = None
        self.__selected_component: Optional[Component] = None

        Tab.__init__(self, parent_notebook, TAB_NAME)
        HasCommonSetup.__init__(self)
        SubscribesToEvents.__init__(self)

    # HasCommonSetup
    def _create_widgets(self) -> None:
        self.__taxonomy_tree = ScrollbarListbox(self,
                                                 on_select_callback=self.__on_select_tree_item,
                                                 heading=TREEVIEW_HEADING,
                                                 extract_id=lambda x: x.id_,
                                                 extract_text=lambda x: x.name,
                                                 extract_ancestor=lambda x: '' if x.parent_id is None else x.parent_id,
                                                 extract_values=self.__extract_values,
                                                 columns=[Column('Amount')],
                                                 values=self.__state.model.taxonomy)
        self.__left_frame = ttk.Frame(self)

        # Ports combobox
        self.__port_combobox_var = tk.StringVar(value=SELECT_PORT)
        self.__port_combobox_var.trace('w', self.__on_combobox_changed)
        # state='readonly' means you cannot write freely in the combobox
        self.__port_combobox = ttk.Combobox(self.__left_frame, state='readonly',
                                            textvariable=self.__port_combobox_var, font=FONT)
        ports_names = self.__state.model.get_all_ports_names()
        self.__port_combobox['values'] = sorted(ports_names)
        # C(r)ud Buttons
        self.__add_port_button = ttk.Button(self.__left_frame, text='Add', state=tk.NORMAL,
                                            command=self.__on_add)
        self.__rename_port_button = ttk.Button(self.__left_frame, text='Rename', state=tk.DISABLED,
                                               command=self.__on_rename)
        self.__remove_port_button = ttk.Button(self.__left_frame, text='Remove', state=tk.DISABLED,
                                               command=self.__remove)
        # Force connection
        self.__force_connection_checkbox_var = tk.BooleanVar(value=False)
        self.__force_connection_checkbox_var.trace('w', self.__on_force_connection_toggled)
        self.__force_connection_checkbox_label = ttk.Label(self.__left_frame,
                                                           text='Force connection:')
        self.__force_connection_checkbox = ttk.Checkbutton(self.__left_frame, state=tk.DISABLED,
                                                           variable=self.__force_connection_checkbox_var)
        # Force connection checkbox
        self.__compatible_with_edit_button = ttk.Button(self.__left_frame, text='Edit compatibility',
                                                        command=self.__on_edit_compatible_with, state=tk.DISABLED)
        self.__compatible_with_listbox = ScrollbarListbox(self.__left_frame,
                                                          extract_text=lambda prt: prt.name,
                                                          extract_id=lambda prt: prt.id_,
                                                          columns=[Column('Compatible with', main=True, stretch=tk.YES)])
        self.__cmp_label_var = tk.StringVar(value='')
        self.__cmp_label = ttk.Label(self.__left_frame, textvariable=self.__cmp_label_var, style='Big.TLabel', anchor=tk.CENTER)

        self.__amount_spinbox_label = ttk.Label(self.__left_frame, text='Has:')
        self.__amount_spinbox_var = tk.IntVar(value='')
        self.__amount_spinbox_var.trace('w', self.__on_amount_changed)
        self.__amount_spinbox = ttk.Spinbox(self.__left_frame, from_=0, to=math.inf,
                                            textvariable=self.__amount_spinbox_var)

        self.__all_children_amount_spinbox_label = ttk.Label(self.__left_frame, text='Children have:')
        self.__all_children_amount_spinbox_var = tk.IntVar(value='')
        self.__all_children_amount_spinbox = ttk.Spinbox(self.__left_frame, from_=0, to=math.inf,
                                                         textvariable=self.__all_children_amount_spinbox_var)
        self.__apply_to_all_children_button = ttk.Button(self.__left_frame, text='Apply to all children',
                                                         command=self.__apply_to_all_children)

    def _setup_layout(self) -> None:
        self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)
        self.__left_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=FRAME_PAD_Y, padx=FRAME_PAD_X)

        self.__port_combobox.grid(row=0, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__add_port_button.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__rename_port_button.grid(row=2, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__remove_port_button.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__force_connection_checkbox_label.grid(row=4, column=0, sticky=tk.W, pady=CONTROL_PAD_Y)
        self.__force_connection_checkbox.grid(row=4, column=1, sticky=tk.E, pady=CONTROL_PAD_Y)
        self.__compatible_with_edit_button.grid(row=5, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__compatible_with_listbox.grid(row=6, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)

        self.__cmp_label.grid(row=7, column=0, columnspan=2, pady=CONTROL_PAD_Y, sticky=tk.EW)
        self.__amount_spinbox_label.grid(row=8, column=0, pady=CONTROL_PAD_Y, sticky=tk.W)
        self.__amount_spinbox.grid(row=8, column=1, pady=CONTROL_PAD_Y, sticky=tk.NSEW)

        self.__all_children_amount_spinbox_label.grid(row=8, column=0, pady=CONTROL_PAD_Y, sticky=tk.W)
        self.__all_children_amount_spinbox.grid(row=8, column=1, pady=CONTROL_PAD_Y, sticky=tk.NSEW)
        self.__apply_to_all_children_button.grid(row=9, column=0, columnspan=2, sticky=tk.NSEW, pady=CONTROL_PAD_Y)

        self.columnconfigure(1, weight=1)     # Give all the remaining space to taxonomy tree
        self.rowconfigure(0, weight=1)

        self.__left_frame.columnconfigure(1, weight=1)

        # Hide widgets
        self.__taxonomy_tree.grid_forget()
        self.__cmp_label.grid_forget()
        self.__amount_spinbox_label.grid_forget()
        self.__amount_spinbox.grid_forget()
        self.__all_children_amount_spinbox_label.grid_forget()
        self.__all_children_amount_spinbox.grid_forget()
        self.__apply_to_all_children_button.grid_forget()

    # Taxonomy Treeview
    def __on_select_tree_item(self, cmp_id: int) -> None:
        """Executed whenever a tree item is selected (by mouse click).

        :param cmp_id: Id of the selected component.
        """
        if self.__selected_port:
            selected_cmp = self.__state.model.get_component(id_=cmp_id)
            self.__selected_component = selected_cmp

            self.__cmp_label.grid(row=7, column=0, columnspan=2, pady=CONTROL_PAD_Y, sticky=tk.EW)  # Show cmp label
            self.__cmp_label_var.set(trim_string(selected_cmp.name, length=30))    # Fill the label with component name

            if selected_cmp.is_leaf:
                self.__all_children_amount_spinbox_label.grid_forget()  # Hide widgets (changing all children)
                self.__all_children_amount_spinbox.grid_forget()
                self.__apply_to_all_children_button.grid_forget()
                self.__all_children_amount_spinbox_var.set(0)
                amount = 0
                if self.__selected_port.id_ in selected_cmp.ports:
                    amount = selected_cmp.ports[self.__selected_port.id_]
                self.__amount_spinbox_var.set(amount)
                self.__amount_spinbox_label.grid(row=8, column=0, pady=CONTROL_PAD_Y, sticky=tk.W)  # Show widgets for leaves
                self.__amount_spinbox.grid(row=8, column=1, pady=CONTROL_PAD_Y, sticky=tk.NSEW)
            else:
                self.__amount_spinbox_label.grid_forget()   # Hide widgets for leaves
                self.__amount_spinbox.grid_forget()

                self.__all_children_amount_spinbox_label.grid(row=8, column=0, pady=CONTROL_PAD_Y, sticky=tk.W) # Show widgets (changing all children)
                self.__all_children_amount_spinbox.grid(row=8, column=1, pady=CONTROL_PAD_Y, sticky=tk.NSEW)
                self.__apply_to_all_children_button.grid(row=9, column=0, columnspan=2, sticky=tk.NSEW,
                                                         pady=CONTROL_PAD_Y)

    def __extract_values(self, cmp: Component) -> Tuple[Any, ...]:
        """Extracts the data of the component to show in the taxonomy view.

        :param cmp: Component from which to extract the data.
        :return: Tuple containing data about component
            (number of ports the of type __selected_port that the component has,).
        """
        amount = ''
        if self.__selected_port:
            if self.__selected_port.id_ in cmp.ports:
                amount = cmp.ports[self.__selected_port.id_]
        return amount,

    def __build_tree(self) -> None:
        """Fills the tree view with components from model."""
        self.__taxonomy_tree.set_items(self.__state.model.taxonomy)

    def __on_combobox_changed(self, *_) -> None:
        """Executed whenever the __port_combobox value changes."""
        # Hide component-specific widgets
        self.__cmp_label.grid_forget()
        self.__amount_spinbox_label.grid_forget()
        self.__amount_spinbox.grid_forget()
        self.__all_children_amount_spinbox_label.grid_forget()
        self.__all_children_amount_spinbox.grid_forget()
        self.__apply_to_all_children_button.grid_forget()

        prt_name = self.__port_combobox_var.get()
        port = self.__state.model.get_port(name=prt_name)
        buttons_to_change_state_of = [
            self.__rename_port_button,
            self.__remove_port_button,
            self.__force_connection_checkbox,
            self.__compatible_with_edit_button,
        ]
        if port:
            self.__selected_port = port
            change_controls_state(tk.NORMAL, *buttons_to_change_state_of)
            compatible_ports = self.__state.model.get_ports_by_ids(port.compatible_with)
            self.__compatible_with_listbox.set_items(compatible_ports)  # Fill the 'compatible with' listbox
            self.__force_connection_checkbox_var.set(port.force_connection)

            self.__update_tree()    # Update tree values
            self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)  # Show tree
        else:
            self.__selected_port = None
            self.__force_connection_checkbox_var.set(False)
            self.__compatible_with_listbox.set_items([])    # Clear 'compatible with' listbox
            change_controls_state(tk.DISABLED,
                                  *buttons_to_change_state_of)
            self.__taxonomy_tree.grid_forget()     # Hide tree

    def __update_tree(self) -> None:
        """Updates every leaf component in the taxonomy treeview."""
        leaf_cmps = self.__state.model.get_components(is_leaf=True)
        self.__taxonomy_tree.update_values(*leaf_cmps)

    # SubscribesToListeners
    def _subscribe_to_events(self) -> None:
        pub.subscribe(self.__on_model_loaded, actions.MODEL_LOADED)
        pub.subscribe(self.__on_taxonomy_edited, actions.TAXONOMY_EDITED)
        pub.subscribe(self._reset, actions.RESET)

    def __on_taxonomy_edited(self):
        """Executed whenever the structure of the taxonomy changes."""
        self.__build_tree()
        self.__selected_component = None
        self.__cmp_label_var.set('')
        self.__cmp_label.grid_forget()
        self.__amount_spinbox_label.grid_forget()
        self.__amount_spinbox.grid_forget()
        self.__all_children_amount_spinbox_label.grid_forget()
        self.__all_children_amount_spinbox.grid_forget()
        self.__apply_to_all_children_button.grid_forget()

    def __on_model_loaded(self):
        """Executed whenever a model is loaded from file."""
        self._reset()
        self.__build_tree()
        ports_names = self.__state.model.get_all_ports_names()
        self.__port_combobox['values'] = sorted(ports_names)

    # Resetable
    def _reset(self) -> None:
        self.__selected_port = None
        self.__selected_component = None
        self.__taxonomy_tree.set_items([])
        self.__port_combobox_var.set(SELECT_PORT)
        self.__port_combobox['values'] = []
        self.__taxonomy_tree.grid_forget()     # Hide treeview

    # Class-specific
    # Ports
    def __on_add(self) -> None:
        """Executed whenever the __add_port_button is pressed."""
        AskStringWindow(self, self.__add, 'Add port', 'Enter name of a new port.')

    def __add(self, name: str) -> None:
        """Executed after creating of a new port.

        :param name: Name of a new port.
        """
        prt = Port(name)
        self.__state.model.add_port(prt)
        self.__selected_port = prt
        self.__port_combobox['values'] = sorted(self.__state.model.get_all_ports_names())
        self.__port_combobox_var.set(prt.name)
        change_controls_state(tk.NORMAL,
                              self.__rename_port_button,
                              self.__remove_port_button,
                              self.__force_connection_checkbox,
                              self.__compatible_with_edit_button)

    def __on_rename(self) -> None:
        """Executed whenever the __rename_port_button is pressed."""
        if self.__selected_port:
            AskStringWindow(self, self.__rename, 'Rename port',
                            f'Enter new name for "{self.__selected_port.name}" port.',
                            string=self.__selected_port.name)

    def __rename(self, new_name: str) -> None:
        """Executed after renaming of a port.

        :param new_name: New name for the __selected_port.
        """
        prt = self.__state.model.rename_port(self.__selected_port, new_name)
        self.__port_combobox['values'] = sorted(self.__state.model.get_all_ports_names())
        self.__port_combobox_var.set(prt.name)

    def __remove(self) -> None:
        """Executed whenever the __remove_port_button is pressed.
        Removes __selected_port from model.
        """
        if self.__selected_port:
            removed_prt = self.__state.model.remove_port(self.__selected_port)
            updated_combobox_values = [val for val in [*self.__port_combobox['values']] if val != removed_prt.name]
            self.__port_combobox['values'] = updated_combobox_values
            self.__selected_port = None
            self.__port_combobox_var.set(SELECT_PORT)
            self.__compatible_with_listbox.set_items([])    # Clear the compatible with
            change_controls_state(tk.DISABLED,
                                  self.__rename_port_button,
                                  self.__remove_port_button,
                                  self.__force_connection_checkbox,
                                  self.__compatible_with_edit_button)

            # Hide component-specific widgets
            self.__cmp_label.grid_forget()
            self.__amount_spinbox_label.grid_forget()
            self.__amount_spinbox.grid_forget()
            self.__all_children_amount_spinbox_label.grid_forget()
            self.__all_children_amount_spinbox.grid_forget()
            self.__apply_to_all_children_button.grid_forget()

            self.__update_tree()

    def __on_edit_compatible_with(self):
        """Executed whenever the __compatible_with_edit_button is pressed."""
        if self.__selected_port:
            all_ports = self.__state.model.ports
            compatible_ports = [p for p in all_ports if p.id_ in self.__selected_port.compatible_with]
            ports_rest = [p for p in all_ports if p not in compatible_ports and p.id_ != self.__selected_port.id_]
            SelectPortsWindow(self, self.__selected_port, compatible_ports, ports_rest,
                              callback=self.__edit_compatible_with)

    def __edit_compatible_with(self, ports: List[Port]) -> None:
        """Executed after editing the list of ports compatible with __selected_port.

        :param ports: New list of ports compatible with __selected_port.
        """
        if self.__selected_port:
            ports.sort(key=lambda x: x.name)
            self.__compatible_with_listbox.set_items(ports)
            self.__state.model.update_ports_compatibility(self.__selected_port, ports)

    def __on_force_connection_toggled(self, *_):
        """Executed whenever the __force_connection_checkbox is toggled."""
        if self.__selected_port:
            value = self.__force_connection_checkbox_var.get()
            self.__selected_port.force_connection = value

    def __on_amount_changed(self, *_):
        """Executed whenever the __amount_spinbox value changes."""
        if self.__selected_component and self.__selected_port:
            value = None
            try:
                value = self.__amount_spinbox_var.get()
            except tk.TclError:
                pass
            finally:
                if value:
                    self.__selected_component.ports[self.__selected_port.id_] = value
                elif self.__selected_port.id_ in self.__selected_component.ports:
                    del self.__selected_component.ports[self.__selected_port.id_]

                self.__taxonomy_tree.update_values(self.__selected_component)

    def __apply_to_all_children(self):
        """Executed whenever the __apply_to_all_children_button is pressed."""
        if self.__selected_component and self.__selected_port:
            value = None
            try:
                value = self.__all_children_amount_spinbox_var.get()
            except tk.TclError:
                pass
            finally:
                updated_components = self.__state.model.set_ports_amount_to_all_components_children(
                    self.__selected_component, self.__selected_port, value)
                self.__taxonomy_tree.update_values(*updated_components)
示例#2
0
class TaxonomyTab(Tab, HasCommonSetup, SubscribesToEvents, Resetable):
    """Used to create, edit and remove components.

    Attributes:
        __selected_component: Currently selected component in the components taxonomy view.
    """
    def __init__(self, parent_notebook):
        self.__state: State = State()
        self.__selected_component: Optional[Component] = None

        Tab.__init__(self, parent_notebook, TAB_NAME)
        HasCommonSetup.__init__(self)
        SubscribesToEvents.__init__(self)

    # HasCommonSetup
    def _create_widgets(self):
        self.__taxonomy_tree = ScrollbarListbox(
            self,
            on_select_callback=self.__on_select_tree_item,
            heading=TREEVIEW_HEADING,
            extract_id=lambda x: x.id_,
            extract_text=lambda x: x.name,
            extract_ancestor=lambda x: ''
            if x.parent_id is None else x.parent_id,
            values=self.__state.model.taxonomy,
        )
        self.__right_frame = ttk.Frame(self)

        self.__cmp_name_var = tk.StringVar()
        self.__cmp_name_var.set('')
        self.__cmp_name_label = ttk.Label(self.__right_frame,
                                          anchor=tk.CENTER,
                                          textvariable=self.__cmp_name_var,
                                          style='Big.TLabel')
        self.__rename_button = ttk.Button(self.__right_frame,
                                          text='Rename',
                                          command=self.__on_rename,
                                          state=tk.DISABLED)
        self.__add_sibling_button = ttk.Button(self.__right_frame,
                                               text='Add sibling',
                                               command=self.__on_add_sibling)
        self.__add_child_button = ttk.Button(self.__right_frame,
                                             text='Add child',
                                             command=self.__on_add_child)
        self.__remove_button = ttk.Button(self.__right_frame,
                                          text='Remove',
                                          command=self.__remove,
                                          state=tk.DISABLED)
        self.__remove_recursively_button = ttk.Button(
            self.__right_frame,
            text='Remove recursively',
            state=tk.DISABLED,
            command=lambda: self.__remove(recursively=True))
        self.__create_taxonomy_button = ttk.Button(
            self,
            text="Create taxonomy",
            command=self.__on_create_taxonomy,
            width=14)

    def _setup_layout(self):
        self.__taxonomy_tree.grid(row=0, column=0, sticky=tk.NSEW)

        self.__right_frame.grid(row=0,
                                column=1,
                                sticky=tk.NSEW,
                                pady=FRAME_PAD_Y,
                                padx=FRAME_PAD_X)

        self.__cmp_name_label.grid(row=0, sticky=tk.EW, pady=CONTROL_PAD_Y)
        self.__rename_button.grid(row=1, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__add_sibling_button.grid(row=2,
                                       sticky=tk.NSEW,
                                       pady=CONTROL_PAD_Y)
        self.__add_child_button.grid(row=3, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__remove_button.grid(row=4, sticky=tk.NSEW, pady=CONTROL_PAD_Y)
        self.__remove_recursively_button.grid(row=5,
                                              sticky=tk.NSEW,
                                              pady=CONTROL_PAD_Y)

        self.__create_taxonomy_button.grid(row=1, column=0, padx=30, pady=5)
        # Hide widgets
        self.rowconfigure(0, weight=1)
        self.columnconfigure(1, weight=1, uniform='fred')
        self.columnconfigure(0, weight=3, uniform='fred')

        self.__right_frame.columnconfigure(0, weight=1)

    # SubscribesToListeners
    def _subscribe_to_events(self):
        pub.subscribe(self.__on_model_loaded, actions.MODEL_LOADED)
        pub.subscribe(self._reset, actions.RESET)

    # Taxonomy Treeview
    def __on_select_tree_item(self, cmp_id: int) -> None:
        """Executed whenever a tree item is selected (by mouse click).

        :param cmp_id: Id of the selected component.
        """
        selected_cmp: Component = self.__state.model.get_component(id_=cmp_id)
        self.__selected_component = selected_cmp
        self.__cmp_name_var.set(trim_string(selected_cmp.name, length=22))
        change_controls_state(tk.NORMAL, self.__remove_button,
                              self.__remove_recursively_button,
                              self.__rename_button)

    def __on_model_loaded(self):
        """Executed whenever a model is loaded from file."""
        self._reset()
        self.__build_tree()

    def __build_tree(self) -> None:
        """Fills the tree view with components from model."""
        self.__taxonomy_tree.set_items(self.__state.model.taxonomy)

    # Resetable
    def _reset(self) -> None:
        self.__taxonomy_tree.set_items([])
        self.__selected_component = None
        self.__cmp_name_var.set('')
        change_controls_state(tk.DISABLED, self.__remove_button,
                              self.__remove_recursively_button,
                              self.__rename_button)

    # Class-specific
    def __add(self, cmp_name: str, level: int,
              parent_id: Optional[int]) -> None:
        """Executed after creating of a new component.

        :param cmp_name: Name of a new component
        :param level: Level of the new component.
        :param parent_id: Id of the parent of the new component.
        """
        cmp = Component(cmp_name,
                        level,
                        parent_id=parent_id,
                        is_leaf=True,
                        symmetry_breaking=True)
        self.__state.model.add_component(cmp)
        self.__taxonomy_tree.add_item(cmp)
        self.__selected_component = cmp
        self.__cmp_name_var.set(trim_string(cmp.name, length=22))
        change_controls_state(tk.NORMAL, self.__remove_button,
                              self.__remove_recursively_button,
                              self.__rename_button)
        pub.sendMessage(actions.TAXONOMY_EDITED)

    def __on_add_sibling(self) -> None:
        """Executed whenever the __add_sibling_button is pressed."""
        sibling_name = BASE_COMPONENT_NAME if self.__selected_component is None else self.__selected_component.name
        level = 0 if self.__selected_component is None else self.__selected_component.level
        parent_id = None if self.__selected_component is None else self.__selected_component.parent_id

        AskStringWindow(
            self, lambda cmp_name: self.__add(cmp_name, level, parent_id),
            'Add sibling',
            f'Enter name of sibling of the {sibling_name} component.')

    def __on_add_child(self) -> None:
        """Executed whenever the __add_child_button is pressed."""
        sibling_name = BASE_COMPONENT_NAME if self.__selected_component is None else self.__selected_component.name
        level = 0 if self.__selected_component is None else self.__selected_component.level + 1
        parent_id = None if self.__selected_component is None else self.__selected_component.id_

        AskStringWindow(
            self, lambda cmp_name: self.__add(cmp_name, level, parent_id),
            'Add sibling',
            f'Enter name of child of the {sibling_name} component.')

    def __rename(self, new_name: str) -> None:
        """Executed after renaming __selected_component.

        :param new_name: New name of the component.
        """
        cmp = self.__state.model.rename_component(self.__selected_component,
                                                  new_name)
        self.__cmp_name_var.set(trim_string(cmp.name, length=22))
        self.__taxonomy_tree.rename_item(self.__selected_component)
        pub.sendMessage(actions.TAXONOMY_EDITED)

    def __on_rename(self) -> None:
        """Executed whenever the __rename_button is pressed."""
        if self.__selected_component:
            AskStringWindow(
                self,
                self.__rename,
                'Rename component',
                f'Enter new name for "{self.__selected_component.name}" component.',
                string=self.__selected_component.name)

    def __remove(self, recursively=False) -> None:
        """Executed whenever the __rename_button or __remove_recursively_button is pressed.

        :param recursively: True if __remove_recursively_button is pressed. Removes the selected component and all of
            its children; Otherwise set to False and removes only the __selected_component, setting the
            parent_id of its children to __selected_component.parent_id.
        """
        if self.__selected_component:
            if recursively:
                _, removed_ctrs = self.__state.model.remove_component_recursively(
                    self.__selected_component)
                self.__taxonomy_tree.remove_item_recursively(
                    self.__selected_component)
            else:
                _, removed_ctrs = self.__state.model.remove_component_preserve_children(
                    self.__selected_component)
                self.__taxonomy_tree.remove_item_preserve_children(
                    self.__selected_component)
            # Prompt about removing the constraints
            if removed_ctrs:
                removed_ctrs_names_list_string = f'    {PUNCTUATOR_SYMBOL} '
                removed_ctrs_names_list_string += f"\n    {PUNCTUATOR_SYMBOL} ".join(
                    [ctr.name for ctr in removed_ctrs])
                messagebox.showwarning(
                    message=f'Removed component was present in constraints.'
                    f'\nThe following constraints were thus removed:'
                    f'\n{removed_ctrs_names_list_string}',
                    parent=self)

            # Disable constrols
            change_controls_state(tk.DISABLED, self.__remove_button,
                                  self.__remove_recursively_button,
                                  self.__rename_button)
            self.__selected_component = None
            self.__cmp_name_var.set('')
            pub.sendMessage(actions.TAXONOMY_EDITED)

    def __create_taxonomy(self):
        """Executes when the taxonomy is created (in the CreateTaxonomyWindow)."""
        self.__build_tree()
        pub.sendMessage(actions.TAXONOMY_EDITED)

    def __on_create_taxonomy(self) -> None:
        """Executed whenever the __create_taxonomy_button is pressed."""
        if self.__state.model.taxonomy:
            answer = messagebox.askyesno(
                'Create taxonomy',
                'Warning: taxonomy has already been created.\n'
                'If you use this option again, previous taxonomy will be '
                "overwritten, and you will lose all constraints' data and "
                "possibly ports, resources, instances, associations"
                'instances etc.\nIf you plan to make simple changes, use '
                'options such as "Create sibling", "Create child", etc. '
                'on the right tab.',
                icon=tk.messagebox.WARNING,
                parent=self)
            if not answer:
                return

        CreateTaxonomyWindow(self, self.__create_taxonomy)
class InstancesTab(Tab,
                   HasCommonSetup,
                   SubscribesToEvents,
                   Resetable):
    """Used to set the number of instances of components.

    Attributes:
        __selected_component: Currently selected component in the components taxonomy view.
    """
    def __init__(self,
                 parent_notebook: ttk.Notebook):
        self.__state = State()
        self.__selected_component: Optional[Component] = None

        Tab.__init__(self, parent_notebook, TAB_NAME)

        HasCommonSetup.__init__(self)
        SubscribesToEvents.__init__(self)

    # HasCommonSetup
    def _create_widgets(self):
        self.__taxonomy_tree = ScrollbarListbox(self,
                                                 on_select_callback=self.__on_select_tree_item,
                                                 heading=TREEVIEW_HEADING,
                                                 extract_id=lambda x: x.id_,
                                                 extract_text=lambda x: x.name,
                                                 extract_ancestor=lambda x: '' if x.parent_id is None else x.parent_id,
                                                 extract_values=self.__extract_values,
                                                 columns=[Column('Count'),
                                                          Column('Min'),
                                                          Column('Max'),
                                                          Column('Symmetry breaking?')],
                                                 values=self.__state.model.taxonomy)

        self.__left_frame = ttk.Frame(self)

        self.__global_symmetry_breaking_frame = ttk.Frame(self.__left_frame)
        self.__global_symmetry_breaking_checkbox_label = ttk.Label(self.__global_symmetry_breaking_frame,
                                                                   text='Symmetry breaking\n'
                                                                        'for all components:')
        self.__global_symmetry_breaking_checkbox_var = tk.BooleanVar(value=False)
        self.__global_symmetry_breaking_checkbox = ttk.Checkbutton(self.__global_symmetry_breaking_frame,
                                                                   variable=self.__global_symmetry_breaking_checkbox_var)
        self.__apply_global_symmetry_breaking_button = ttk.Button(self.__global_symmetry_breaking_frame, text='Apply',
                                                                  command=self.__apply_symmetry_breaking_for_all_components)

        self.__component_separator = ttk.Separator(self.__left_frame, orient=tk.HORIZONTAL)

        self.__cmp_label_var = tk.StringVar(value='')
        self.__cmp_label = ttk.Label(self.__left_frame, textvariable=self.__cmp_label_var, style='Big.TLabel',
                                     anchor=tk.CENTER)

        self.__symm_breaking_checkbox_var = tk.BooleanVar(value=False)
        self.__symm_breaking_checkbox_var.trace('w', self.__on_symmetry_breaking_toggled)
        self.__symm_breaking_checkbox_label = ttk.Label(self.__left_frame, text='Symmetry\nbreaking:')
        self.__symm_breaking_checkbox = ttk.Checkbutton(self.__left_frame, variable=self.__symm_breaking_checkbox_var)

        self.__exact_value_radiobutton_var = tk.BooleanVar(value=True)
        self.__exact_value_radiobutton_var.trace('w', self.__on_exact_value_radiobutton_changed)
        self.__exact_value_radiobutton = ttk.Radiobutton(self.__left_frame, value=True, text='Exact', state=tk.DISABLED,
                                                         variable=self.__exact_value_radiobutton_var)
        self.__range_radiobutton = ttk.Radiobutton(self.__left_frame, text='Range', value=False, state=tk.DISABLED,
                                                   variable=self.__exact_value_radiobutton_var)

        self.__exact_minimum_spinbox_label_var = tk.StringVar(value=EXACT_COUNT_LABEL_TEXT)
        self.__exact_minimum_spinbox_label = ttk.Label(self.__left_frame,
                                                       textvariable=self.__exact_minimum_spinbox_label_var)
        self.__exact_minimum_spinbox_var = tk.IntVar(value='')
        self.__exact_minimum_spinbox_var.trace('w', self.__on_count_changed)
        self.__exact_minimum_spinbox = ttk.Spinbox(self.__left_frame, from_=0, to=math.inf, state=tk.DISABLED,
                                                   textvariable=self.__exact_minimum_spinbox_var)

        self.__max_spinbox_label = ttk.Label(self.__left_frame, text='Max:')
        self.__max_spinbox_var = tk.IntVar()
        self.__max_spinbox_var.trace('w', self.__on_max_changed)
        self.__max_spinbox = ttk.Spinbox(self.__left_frame, from_=1, to=math.inf,
                                         textvariable=self.__max_spinbox_var)

        self.__apply_symmetry_breaking_to_all_children_button = ttk.Button(self.__left_frame,
                                                                           text='Apply to children',
                                                                           command=self.__apply_symmetry_breaking_to_all_children,
                                                                           style='SmallFont.TButton')

        self.__apply_count_to_all_children_button = ttk.Button(self.__left_frame, text='Apply to all children',
                                                               command=self.__apply_count_to_all_children)

    def _setup_layout(self):
        self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)
        self.__left_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=FRAME_PAD_X, pady=FRAME_PAD_Y)

        self.__global_symmetry_breaking_frame.grid(row=0, column=0, columnspan=4, sticky=tk.NSEW)
        self.__global_symmetry_breaking_frame.columnconfigure(0, weight=1)
        self.__global_symmetry_breaking_frame.columnconfigure(1, weight=1)
        self.__global_symmetry_breaking_frame.columnconfigure(2, weight=1)

        self.__global_symmetry_breaking_checkbox_label.grid(row=0, column=0, sticky=tk.W, pady=CONTROL_PAD_Y, padx=CONTROL_PAD_X)
        self.__global_symmetry_breaking_checkbox.grid(row=0, column=1, sticky=tk.W, pady=CONTROL_PAD_Y, padx=CONTROL_PAD_X)
        self.__apply_global_symmetry_breaking_button.grid(row=0, column=2, sticky=tk.EW, pady=CONTROL_PAD_Y, padx=CONTROL_PAD_X)

        self.__component_separator.grid(row=1, column=0, columnspan=4, sticky=tk.EW, pady=CONTROL_PAD_Y, padx=CONTROL_PAD_X)

        self.__cmp_label.grid(row=2, column=0, columnspan=4, sticky=tk.EW, pady=CONTROL_PAD_Y)

        self.__symm_breaking_checkbox_label.grid(row=3, column=0, sticky=tk.W, pady=CONTROL_PAD_Y, padx=CONTROL_PAD_X)
        self.__symm_breaking_checkbox.grid(row=3, column=1, sticky=tk.NW, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
        self.__apply_symmetry_breaking_to_all_children_button.grid(row=3, column=2, columnspan=2, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)

        self.__exact_value_radiobutton.grid(row=4, column=0, sticky=tk.W, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
        self.__range_radiobutton.grid(row=4, column=1, sticky=tk.W, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)

        self.__exact_minimum_spinbox_label.grid(row=5, column=0, sticky=tk.W, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
        self.__exact_minimum_spinbox.grid(row=5, column=1, sticky=tk.EW, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
        self.__max_spinbox_label.grid(row=5, column=2, sticky=tk.W, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
        self.__max_spinbox.grid(row=5, column=3, sticky=tk.EW, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)

        self.__apply_count_to_all_children_button.grid(row=6, column=0, columnspan=4, sticky=tk.NSEW, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)

        # Hide 'for all children' widgets
        self.__apply_symmetry_breaking_to_all_children_button.grid_forget()
        self.__max_spinbox_label.grid_forget()
        self.__max_spinbox.grid_forget()
        self.__apply_count_to_all_children_button.grid_forget()

        self.__left_frame.columnconfigure(0, weight=1, uniform='fred')
        self.__left_frame.columnconfigure(1, weight=1, uniform='fred')
        self.__left_frame.columnconfigure(2, weight=1, uniform='fred')
        self.__left_frame.columnconfigure(3, weight=1, uniform='fred')

        self.columnconfigure(0, weight=1, uniform='fred')
        self.columnconfigure(1, weight=3, uniform='fred')
        self.rowconfigure(0, weight=1)

    # SubscribesToListeners
    def _subscribe_to_events(self):
        pub.subscribe(self.__on_model_loaded, actions.MODEL_LOADED)
        pub.subscribe(self.__build_tree, actions.TAXONOMY_EDITED)
        pub.subscribe(self._reset, actions.RESET)

    @staticmethod
    def __extract_values(cmp: Component) -> Tuple[Any, ...]:
        """Extracts the data of the component to show in the taxonomy view.

        :param cmp: Component from which to extract the data.
        :return: Tuple containing data about component
            (exact count, min_count, max_count, apply symmetry breaking?).
        """
        return (cmp.count if cmp.count else '',
                cmp.min_count if cmp.min_count is not None else '',
                cmp.max_count if cmp.max_count is not None else '',
                BOOLEAN_TO_STRING_DICT[cmp.symmetry_breaking])

    def __on_model_loaded(self) -> None:
        """Executed whenever a model is loaded from file."""
        self.__build_tree()

    def __build_tree(self) -> None:
        """Fills the tree view with components from model."""
        self.__taxonomy_tree.set_items(self.__state.model.taxonomy)

    # Taxonomy Treeview
    def __on_select_tree_item(self, cmp_id: int) -> None:
        """Executed whenever a tree item is selected (by mouse click).
        
        :param cmp_id: Id of the selected component.
        """
        selected_cmp: Component = self.__state.model.get_component(id_=cmp_id)
        self.__selected_component = selected_cmp
        self.__cmp_label_var.set(trim_string(selected_cmp.name, length=LABEL_LENGTH))

        radiobutton_val = selected_cmp.exact if selected_cmp.exact is not None else True
        self.__exact_value_radiobutton_var.set(radiobutton_val)
        # Enable symmetry breaking checkbox & spinbox
        change_controls_state(tk.NORMAL,
                              self.__exact_minimum_spinbox,
                              self.__symm_breaking_checkbox,
                              self.__range_radiobutton,
                              self.__exact_value_radiobutton,
                              self.__exact_minimum_spinbox)
        if selected_cmp.is_leaf:
            # Set the spinbox values
            if selected_cmp.exact:
                set_spinbox_var_value(self.__exact_minimum_spinbox_var, selected_cmp.count)
            else:
                set_spinbox_var_value(self.__exact_minimum_spinbox_var, selected_cmp.min_count)
                set_spinbox_var_value(self.__max_spinbox_var, selected_cmp.max_count)

            self.__symm_breaking_checkbox_var.set(selected_cmp.symmetry_breaking)
            self.__apply_symmetry_breaking_to_all_children_button.grid_forget()
            self.__apply_count_to_all_children_button.grid_forget()
        else:
            # Show apply to all children
            self.__apply_symmetry_breaking_to_all_children_button.grid(row=3, column=2, columnspan=2, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
            self.__apply_count_to_all_children_button.grid(row=6, column=0, columnspan=4, sticky=tk.NSEW, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
            # Reset symmetry breaking for all components
            self.__symm_breaking_checkbox_var.set(False)

    # Resetable
    def _reset(self) -> None:
        self.__selected_component = None
        # Disable widgets
        change_controls_state(tk.DISABLED,
                              self.__exact_minimum_spinbox,
                              self.__symm_breaking_checkbox,
                              self.__range_radiobutton,
                              self.__exact_value_radiobutton,
                              self.__exact_minimum_spinbox)

        # Hide 'for all children' widgets
        self.__apply_symmetry_breaking_to_all_children_button.grid_forget()
        self.__max_spinbox_label.grid_forget()
        self.__max_spinbox.grid_forget()
        self.__apply_count_to_all_children_button.grid_forget()

        # Set entries to default
        self.__exact_minimum_spinbox_var.set('')
        self.__max_spinbox_var.set('')
        self.__cmp_label_var.set('')
        self.__symm_breaking_checkbox_var.set(False)
        self.__global_symmetry_breaking_checkbox_var.set(False)
        # Clear the tree
        self.__taxonomy_tree.set_items([])

    def __on_symmetry_breaking_toggled(self, *_) -> None:
        """Executed whenever the __symmetry_breaking_checkbox is toggled"""
        if self.__selected_component and self.__selected_component.is_leaf:
            self.__selected_component.symmetry_breaking = self.__symm_breaking_checkbox_var.get()
            self.__taxonomy_tree.update_values(self.__selected_component)

    def __on_count_changed(self, *_) -> None:
        """Executed whenever the __exact_minimum_spinbox value changes."""
        if self.__selected_component:
            exact = self.__exact_value_radiobutton_var.get()
            try:
                value = self.__exact_minimum_spinbox_var.get()
                if exact:
                    if self.__selected_component.is_leaf:
                        self.__selected_component.count = value
                        # Reset min/max counter
                        self.__selected_component.min_count = None
                        self.__selected_component.max_count = None

                else:
                    max_value = self.__max_spinbox_var.get()
                    if self.__selected_component.is_leaf:
                        self.__selected_component.min_count = value
                        # Reset count property
                        self.__selected_component.count = None
                    if value >= max_value:
                        new_max_value = value + 1
                        self.__max_spinbox_var.set(new_max_value)
                        if self.__selected_component.is_leaf:
                            self.__selected_component.max_count = new_max_value
            except tk.TclError:
                self.__selected_component.count = None  # Reset both
                self.__selected_component.min_count = None
            finally:
                self.__taxonomy_tree.update_values(self.__selected_component)

    def __apply_count_to_all_children(self) -> None:
        """Executed whenever the __apply_count_to_all_children_button is pressed."""
        if self.__selected_component:
            exact = self.__exact_value_radiobutton_var.get()
            if exact:
                count = None
                try:
                    count = self.__exact_minimum_spinbox_var.get()
                except tk.TclError:
                    pass
                finally:
                    updated_cmps = self.__state.model.set_components_leaf_children_properties(self.__selected_component,
                                                                                              exact=True, count=count,
                                                                                              min_count=None,
                                                                                              max_count=None)
            else:
                min_count = None
                max_count = None
                try:
                    min_count = self.__exact_minimum_spinbox_var.get()
                    max_count = self.__max_spinbox_var.get()
                except tk.TclError:
                    pass
                finally:
                    updated_cmps = self.__state.model.set_components_leaf_children_properties(self.__selected_component,
                                                                                              exact=False,
                                                                                              count=None,
                                                                                              min_count=min_count,
                                                                                              max_count=max_count)
            self.__taxonomy_tree.update_values(*updated_cmps)

    def __apply_symmetry_breaking_to_all_children(self):
        """Executed whenever the __apply_symmetry_breaking_to_all_children_button is pressed."""
        if self.__selected_component:
            symm_breaking = True
            try:
                symm_breaking = self.__symm_breaking_checkbox_var.get()
            except tk.TclError:
                pass
            finally:
                updated_cmps = self.__state.model.set_components_leaf_children_properties(self.__selected_component,
                                                                                          symmetry_breaking=symm_breaking)
                self.__taxonomy_tree.update_values(*updated_cmps)

    def __on_exact_value_radiobutton_changed(self, *_):
        """Executed whenever the __exact_value_radiobutton's value changes."""
        if self.__selected_component:
            self.__exact_minimum_spinbox_var.set(0)
            self.__max_spinbox_var.set(0)

            self.__selected_component.count = None
            self.__selected_component.min_count = None
            self.__selected_component.max_count = None

            exact_value = self.__exact_value_radiobutton_var.get()
            if exact_value:
                self.__max_spinbox_label.grid_forget()  # Hide 'Max' label and spinbox
                self.__max_spinbox.grid_forget()
                self.__exact_minimum_spinbox_label_var.set(EXACT_COUNT_LABEL_TEXT)
            else:
                # Restore max widgets
                self.__max_spinbox_label.grid(row=5, column=2, sticky=tk.W, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
                self.__max_spinbox.grid(row=5, column=3, sticky=tk.EW, padx=CONTROL_PAD_X, pady=CONTROL_PAD_Y)
                self.__exact_minimum_spinbox_label_var.set(MIN_COUNT_LABEL_TEXT)

            if self.__selected_component.is_leaf:
                self.__selected_component.exact = exact_value
                self.__taxonomy_tree.update_values(self.__selected_component)

    def __on_max_changed(self, *_):
        """Executed whenever the __max_spinbox value changes."""
        if self.__selected_component:
            try:
                max_value = self.__max_spinbox_var.get()
                if self.__selected_component.is_leaf:
                    self.__selected_component.max_count = max_value

                min_value = self.__exact_minimum_spinbox_var.get()
                if max_value <= min_value and max_value >= 1:
                    new_min_value = max_value - 1
                    self.__exact_minimum_spinbox_var.set(new_min_value)
                    if self.__selected_component.is_leaf:
                        self.__selected_component.min_count = new_min_value
            except tk.TclError:
                pass
            finally:
                self.__taxonomy_tree.update_values(self.__selected_component)

    def __apply_symmetry_breaking_for_all_components(self):
        """Executed whenever the __apply_global_symmetry_breaking_button is pressed."""
        if self.__taxonomy_tree:
            symm_breaking = self.__global_symmetry_breaking_checkbox_var.get()
            updated_cmps = self.__state.model.set_all_leaf_components_properties(symmetry_breaking=symm_breaking)
            self.__taxonomy_tree.update_values(*updated_cmps)
class SelectPortsWindow(HasCommonSetup, Window):
    """Windows used to create (or edit) a subset of a set of ports..

    Attributes:
        __callback: Callback function to be executed after pressing the OK button on this Window.
        __ports_right: Set of selected ports.
        __ports_left: Set of not-selected ports.
        __selected_port_left: Currently selected port from __ports_right.
        __selected_port_right: Currently selected port from __ports_left.
    """
    def __init__(self, parent_frame, selected_port: Port,
                 ports_right: List[Port], ports_left: List[Port],
                 callback: Callable[[List[Port]], Any]):
        self.__state: State = State()
        self.__callback: Callable[[List[Port]], Any] = callback

        self.__ports_right: List[Port] = ports_right
        self.__ports_left: List[Port] = ports_left

        self.__selected_port_left: Optional[Port] = None
        self.__selected_port_right: Optional[Port] = None

        Window.__init__(self, parent_frame,
                        WINDOW_TITLE.format(selected_port.name))
        HasCommonSetup.__init__(self)

    # HasCommonSetup
    def _create_widgets(self) -> None:
        self.__ports_left_listbox = ScrollbarListbox(
            self,
            values=self.__ports_left,
            extract_id=lambda prt: prt.id_,
            extract_text=lambda prt: prt.name,
            on_select_callback=self.__on_select_listbox_left,
            columns=[Column('Port', main=True, stretch=tk.YES)])
        self.__mid_frame = ttk.Frame(self)

        self.__remove_port_button = ttk.Button(
            self.__mid_frame, text='<<', command=self.__remove_from_selected)
        self.__add_port_button = ttk.Button(self.__mid_frame,
                                            text='>>',
                                            command=self.__add_to_selected)

        self.__right_frame = ttk.Frame(self)
        self.__ports_right_listbox = ScrollbarListbox(
            self.__right_frame,
            values=self.__ports_right,
            extract_id=lambda prt: prt.id_,
            extract_text=lambda prt: prt.name,
            on_select_callback=self.__on_select_listbox_right,
            columns=[Column('Selected ports', main=True, stretch=tk.YES)])
        self.__buttons_frame = ttk.Frame(self.__right_frame)
        self.__ok_button = ttk.Button(self.__buttons_frame,
                                      text='Ok',
                                      command=self.__ok)
        self.__cancel_button = ttk.Button(self.__buttons_frame,
                                          text='Cancel',
                                          command=self.destroy)

    def _setup_layout(self) -> None:
        self.__ports_left_listbox.grid(row=0,
                                       column=0,
                                       sticky=tk.NSEW,
                                       padx=FRAME_PAD_X,
                                       pady=FRAME_PAD_Y)
        self.__mid_frame.grid(row=0, column=1)
        self.__remove_port_button.grid(row=1, column=0, pady=CONTROL_PAD_Y)
        self.__add_port_button.grid(row=2, column=0, pady=CONTROL_PAD_Y)

        self.__right_frame.grid(row=0,
                                column=2,
                                sticky=tk.NSEW,
                                padx=FRAME_PAD_X,
                                pady=FRAME_PAD_Y)
        self.__ports_right_listbox.grid(row=0,
                                        column=0,
                                        sticky=tk.NSEW,
                                        pady=CONTROL_PAD_Y)
        self.__buttons_frame.grid(row=2, column=0, sticky=tk.NSEW)
        self.__ok_button.grid(row=0,
                              column=0,
                              sticky=tk.NSEW,
                              pady=CONTROL_PAD_Y)
        self.__cancel_button.grid(row=0,
                                  column=1,
                                  sticky=tk.NSEW,
                                  pady=CONTROL_PAD_Y)

        self.__right_frame.grid_columnconfigure(0, weight=1)
        self.__right_frame.grid_rowconfigure(0, weight=1)

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=2, uniform='fred')
        self.columnconfigure(1, weight=1, uniform='fred')
        self.columnconfigure(2, weight=2, uniform='fred')

        self.__buttons_frame.columnconfigure(0, weight=1)
        self.__buttons_frame.columnconfigure(1, weight=1)

        self._set_geometry()

    def __on_select_listbox_left(self, prt_id: int) -> None:
        """Executed whenever a __ports_left_listbox item is selected (by mouse click).

        :param prt_id: Id of the selected port.
        """
        self.__selected_port_left = self.__state.model.get_port(id_=prt_id)

    def __on_select_listbox_right(self, prt_id: int) -> None:
        """Executed whenever a __ports_right_listbox item is selected (by mouse click).

        :param prt_id: Id of the selected port.
        """
        self.__selected_port_right = self.__state.model.get_port(id_=prt_id)

    def __add_to_selected(self):
        """Removes the __selected_port_left from __ports_left and adds it to __ports_right."""
        if self.__selected_port_left:
            prt = self.__selected_port_left
            self.__ports_left.remove(prt)
            self.__ports_left_listbox.remove_item_recursively(prt)
            self.__selected_port_left = None
            self.__ports_right.append(prt)
            self.__selected_port_right = prt
            right_port_names = sorted([p.name for p in self.__ports_right])
            index = right_port_names.index(prt.name)
            self.__ports_right_listbox.add_item(prt, index=index)

    def __remove_from_selected(self):
        """Removes the __selected_port_right from __ports_right and adds it to __ports_left."""
        if self.__selected_port_right:
            prt = self.__selected_port_right
            self.__ports_right.remove(prt)
            self.__ports_right_listbox.remove_item_recursively(prt)
            self.__selected_port_right = None
            self.__ports_left.append(prt)
            self.__selected_port_left = prt
            left_port_names = sorted([p.name for p in self.__ports_left])
            index = left_port_names.index(prt.name)
            self.__ports_left_listbox.add_item(prt, index=index)

    def __ok(self):
        """Executed whenever the __ok_button is pressed."""
        self.grab_release()
        self.__callback(self.__ports_right)
        self.destroy()
示例#5
0
class SimpleConstraintWindow(HasCommonSetup, Window):
    """Used create/edit SimpleConstraints.

    Attributes:
        __callback: Callback function to be executed after pressing the OK button on this Window.
        __constraint: SimpleConstraint to add / create.
        __components_ids: List of component ids concerned by this constraint.
        __selected_taxonomy_tree_item: Currently selected component in the components taxonomy view.
        __selected_listbox_item: Currently selected component in the selected components listview.
    """
    def __init__(self,
                 parent_frame,
                 callback: Optional[Callable],
                 constraint: Optional[SimpleConstraint] = None):
        self.__state = State()
        self.__callback = callback
        self.__constraint: SimpleConstraint = copy.deepcopy(constraint) if constraint is not None \
            else SimpleConstraint()
        self.__components_ids = [] if constraint is None else [
            *constraint.components_ids
        ]  # Deep copy of component ids
        self.__selected_taxonomy_tree_item: Optional[Component] = None
        self.__selected_listbox_item: Optional[Component] = None

        Window.__init__(self, parent_frame, WINDOW_TITLE)
        HasCommonSetup.__init__(self)

    # HasCommonSetup
    def _create_widgets(self) -> None:
        self.__taxonomy_tree = ScrollbarListbox(
            self,
            on_select_callback=self.__on_select_tree_item,
            heading=TREEVIEW_HEADING,
            extract_id=lambda x: x.id_,
            extract_text=lambda x: x.name,
            extract_ancestor=lambda x: ''
            if x.parent_id is None else x.parent_id,
            values=self.__state.model.taxonomy)

        self.__mid_frame = ttk.Frame(self)
        self.__add_component_button = ttk.Button(
            self.__mid_frame, text='>>', command=self.__add_to_selected)
        self.__add_components_recursively_button = ttk.Button(
            self.__mid_frame,
            text='>> (recursively)',
            command=self.__add_to_selected_recursively)
        self.__remove_component_button = ttk.Button(
            self.__mid_frame, text='<<', command=self.__remove_from_selected)

        self.__right_frame = ttk.Frame(self)

        self.__components_listbox = ScrollbarListbox(
            self.__right_frame,
            values=self.__state.model.get_components_by_ids(
                self.__constraint.components_ids),
            extract_id=lambda cmp: cmp.id_,
            extract_text=lambda cmp: cmp.name,
            on_select_callback=self.__on_select_listbox_component,
            columns=[Column('Selected components', main=True, stretch=tk.YES)])

        # Name
        self.__name_entry_var = tk.StringVar(value=self.__constraint.name)
        self.__name_entry_label = ttk.Label(self.__right_frame, text='Name:')
        self.__name_entry = ttk.Entry(self.__right_frame,
                                      textvariable=self.__name_entry_var,
                                      font=FONT)
        # Description
        self.__description_text_label = ttk.Label(self.__right_frame,
                                                  text='Description:')
        self.__description_text = tk.Text(self.__right_frame,
                                          height=2,
                                          font=FONT)
        if self.__constraint.description:
            self.__description_text.insert(tk.INSERT,
                                           self.__constraint.description)
        # Distinct checkbox
        self.__distinct_checkbox_var = tk.BooleanVar(
            value=self.__constraint.distinct)
        self.__distinct_checkbox_label = ttk.Label(self.__right_frame,
                                                   text='Distinct?')
        self.__distinct_checkbox = ttk.Checkbutton(
            self.__right_frame, variable=self.__distinct_checkbox_var)
        # Has min checkbox
        self.__has_min_checkbox_var = tk.BooleanVar(
            value=self.__constraint.min_ is not None)
        self.__has_min_checkbox_var.trace('w', self.__on_has_min_changed)
        self.__has_min_checkbox_label = ttk.Label(self.__right_frame,
                                                  text='Has min?')
        self.__has_min_checkbox = ttk.Checkbutton(
            self.__right_frame, variable=self.__has_min_checkbox_var)
        # Min spinbox
        min_var_value = self.__constraint.min_ if self.__constraint.min_ is not None else ''
        self.__min_spinbox_var = tk.IntVar(value=min_var_value)
        self.__min_spinbox_var.trace('w', self.__on_min_changed)
        self.__min_spinbox = ttk.Spinbox(
            self.__right_frame,
            from_=0,
            to=math.inf,
            textvariable=self.__min_spinbox_var,
            state=tk.NORMAL
            if self.__constraint.min_ is not None else tk.DISABLED)
        # Has max checkbox
        self.__has_max_checkbox_var = tk.BooleanVar(
            value=self.__constraint.max_ is not None)
        self.__has_max_checkbox_var.trace('w', self.__on_has_max_changed)
        self.__has_max_checkbox_label = ttk.Label(self.__right_frame,
                                                  text='Has max?')
        self.__has_max_checkbox = ttk.Checkbutton(
            self.__right_frame, variable=self.__has_max_checkbox_var)
        # Max spinbox
        max_var_value = self.__constraint.max_ if self.__constraint.max_ is not None else ''
        self.__max_spinbox_var = tk.IntVar(value=max_var_value)
        self.__max_spinbox_var.trace('w', self.__on_max_changed)
        self.__max_spinbox = ttk.Spinbox(
            self.__right_frame,
            from_=0,
            to=math.inf,
            textvariable=self.__max_spinbox_var,
            state=tk.NORMAL
            if self.__constraint.max_ is not None else tk.DISABLED)
        # Buttons frame
        self.__ok_button = ttk.Button(self.__right_frame,
                                      text='Ok',
                                      command=self.__ok)
        self.__cancel_button = ttk.Button(self.__right_frame,
                                          text='Cancel',
                                          command=self.destroy)

    def _setup_layout(self) -> None:
        self.__taxonomy_tree.grid(row=0,
                                  column=0,
                                  sticky=tk.NSEW,
                                  pady=FRAME_PAD_Y,
                                  padx=FRAME_PAD_X)
        self.__mid_frame.grid(row=0, column=1)
        self.__remove_component_button.grid(row=1,
                                            column=0,
                                            sticky=tk.NSEW,
                                            pady=CONTROL_PAD_Y)
        self.__add_component_button.grid(row=2,
                                         column=0,
                                         sticky=tk.NSEW,
                                         pady=CONTROL_PAD_Y)
        self.__add_components_recursively_button.grid(row=3,
                                                      column=0,
                                                      sticky=tk.NSEW,
                                                      pady=CONTROL_PAD_Y)

        self.__right_frame.grid(row=0,
                                column=2,
                                sticky=tk.NSEW,
                                pady=FRAME_PAD_Y,
                                padx=FRAME_PAD_X)
        self.__name_entry_label.grid(row=0,
                                     column=0,
                                     sticky=tk.W,
                                     pady=CONTROL_PAD_Y)
        self.__name_entry.grid(row=0,
                               column=1,
                               columnspan=3,
                               sticky=tk.NSEW,
                               pady=CONTROL_PAD_Y)
        self.__description_text_label.grid(row=1,
                                           column=0,
                                           sticky=tk.NW,
                                           pady=CONTROL_PAD_Y)
        self.__description_text.grid(row=1,
                                     column=1,
                                     columnspan=3,
                                     sticky=tk.NSEW,
                                     pady=CONTROL_PAD_Y)
        self.__distinct_checkbox_label.grid(row=2,
                                            column=0,
                                            sticky=tk.W,
                                            pady=CONTROL_PAD_Y)
        self.__distinct_checkbox.grid(row=2,
                                      column=1,
                                      columnspan=3,
                                      sticky=tk.E,
                                      pady=CONTROL_PAD_Y)

        self.__has_min_checkbox_label.grid(row=3,
                                           column=0,
                                           sticky=tk.W,
                                           pady=CONTROL_PAD_Y)
        self.__has_min_checkbox.grid(row=3,
                                     column=1,
                                     sticky=tk.E,
                                     pady=CONTROL_PAD_Y)
        self.__has_max_checkbox_label.grid(row=3,
                                           column=2,
                                           sticky=tk.W,
                                           pady=CONTROL_PAD_Y)
        self.__has_max_checkbox.grid(row=3,
                                     column=3,
                                     sticky=tk.E,
                                     pady=CONTROL_PAD_Y)
        self.__min_spinbox.grid(row=4,
                                column=0,
                                columnspan=2,
                                sticky=tk.NSEW,
                                pady=CONTROL_PAD_Y)
        self.__max_spinbox.grid(row=4,
                                column=2,
                                columnspan=2,
                                sticky=tk.NSEW,
                                pady=CONTROL_PAD_Y)

        self.__components_listbox.grid(row=5,
                                       column=0,
                                       columnspan=4,
                                       sticky=tk.NSEW,
                                       pady=CONTROL_PAD_Y)

        self.__ok_button.grid(row=6,
                              column=0,
                              columnspan=2,
                              sticky=tk.EW,
                              pady=CONTROL_PAD_Y)
        self.__cancel_button.grid(row=6,
                                  column=2,
                                  columnspan=2,
                                  sticky=tk.EW,
                                  pady=CONTROL_PAD_Y)

        self.__right_frame.rowconfigure(1, weight=1)
        self.__right_frame.rowconfigure(5, weight=2)

        self.__right_frame.columnconfigure(1, weight=1)
        self.__right_frame.columnconfigure(3, weight=1)

        self.columnconfigure(0, weight=1, uniform='fred')
        self.columnconfigure(2, weight=1, uniform='fred')
        self.rowconfigure(0, weight=1)

        self._set_geometry()

    # Taxonomy Treeview
    def __on_select_tree_item(self, cmp_id: int) -> None:
        """Executed whenever a tree item is selected (by mouse click).

        :param cmp_id: Id of the selected component.
        """
        self.__selected_taxonomy_tree_item = self.__state.model.get_component(
            id_=cmp_id)

    def __on_select_listbox_component(self, cmp_id: int) -> None:
        """Executed whenever a listview item is selected (by mouse click).

        :param cmp_id: Id of the selected component.
        """
        self.__selected_listbox_item = self.__state.model.get_component(
            id_=cmp_id)

    def __add_to_selected(self):
        """Adds the __selected_taxonomy_tree_item to the list of selected component ids."""
        if self.__selected_taxonomy_tree_item:
            if self.__selected_taxonomy_tree_item.id_ not in self.__components_ids:
                self.__components_ids.append(
                    self.__selected_taxonomy_tree_item.id_)
                self.__components_listbox.add_item(
                    self.__selected_taxonomy_tree_item)
                self.__selected_listbox_item = self.__selected_taxonomy_tree_item
                self.__selected_taxonomy_tree_item = None

    def __add_to_selected_recursively(self):
        """Adds the __selected_taxonomy_tree_item and all its children to the list of selected component ids."""
        if self.__selected_taxonomy_tree_item:
            for c in self.__state.model.get_components_children(
                    self.__selected_taxonomy_tree_item):
                if c.id_ not in self.__components_ids:
                    self.__components_ids.append(c.id_)
                    self.__components_listbox.add_item(
                        c, select_item=False)  # Append children
            self.__components_ids.append(
                self.__selected_taxonomy_tree_item.id_)
            self.__components_listbox.add_item(
                self.__selected_taxonomy_tree_item)
            self.__selected_listbox_item = self.__selected_taxonomy_tree_item
            self.__selected_taxonomy_tree_item = None

    def __remove_from_selected(self):
        """Adds the __selected_taxonomy_tree_item to the list of selected component ids."""
        if self.__selected_listbox_item:
            self.__components_ids.remove(self.__selected_listbox_item.id_)
            self.__components_listbox.remove_item_recursively(
                self.__selected_listbox_item)
            self.__selected_taxonomy_tree_item = self.__selected_listbox_item
            self.__taxonomy_tree.select_item(self.__selected_listbox_item)
            self.__selected_listbox_item = None

    def __on_has_min_changed(self, *_):
        """Executed whenever the __has_min_checkbox is toggled"""
        has_min = self.__has_min_checkbox_var.get()
        if has_min:
            self.__min_spinbox.config(state=tk.ACTIVE)
            self.__min_spinbox_var.set(0)
        else:
            self.__min_spinbox_var.set('')
            self.__min_spinbox.config(state=tk.DISABLED)

    def __on_has_max_changed(self, *_):
        """Executed whenever the __has_max_checkbox is toggled"""
        has_max = self.__has_max_checkbox_var.get()
        if has_max:
            self.__max_spinbox.config(state=tk.ACTIVE)
            has_min = self.__has_min_checkbox_var.get()
            if has_min:
                min_ = self.__min_spinbox_var.get()
                self.__max_spinbox_var.set(min_)
            else:
                self.__max_spinbox_var.set(0)
                self.__constraint.max_ = 0
        else:
            self.__max_spinbox_var.set('')
            self.__max_spinbox.config(state=tk.DISABLED)

    def __on_min_changed(self, *_):
        """Executed whenever the __min_spinbox value changes."""
        try:
            min_ = self.__min_spinbox_var.get()
            has_max = self.__has_max_checkbox_var.get()
            if has_max:
                max_ = self.__max_spinbox_var.get()
                if min_ > max_:
                    self.__max_spinbox_var.set(min_)
        except tk.TclError:
            pass

    def __on_max_changed(self, *_):
        """Executed whenever the __max_spinbox value changes."""
        try:
            max_ = self.__max_spinbox_var.get()
            has_min = self.__has_min_checkbox_var.get()
            if has_min:
                min_ = self.__min_spinbox_var.get()
                if min_ > max_:
                    self.__min_spinbox_var.set(max_)
        except tk.TclError:
            pass

    def __ok(self):
        """Executed whenever the __ok_button is pressed."""
        min_ = None
        max_ = None
        if self.__has_min_checkbox_var.get():  # If component has min
            try:
                min_ = self.__min_spinbox_var.get()
            except tk.TclError:
                min_ = None
        if self.__has_max_checkbox_var.get():  # If component has max
            try:
                max_ = self.__max_spinbox_var.get()
            except tk.TclError:
                max_ = None
        try:
            name = self.__name_entry_var.get()
            # Rewrite values if they are correct
            self.__constraint.name = name
            self.__constraint.description = self.__description_text.get(
                1.0, tk.END)
            self.__constraint.components_ids = self.__components_ids
            self.__constraint.distinct = self.__distinct_checkbox_var.get()
            self.__constraint.min_ = min_
            self.__constraint.max_ = max_

            #   If this SimpleConstraint is not an independent constraint, but a part of a ComplexConstraint,
            #   name is checked against the names of other SimpleConstraints in the antecedent/consequent
            #   of the ComplexConstraint,
            #   Otherwise, it is checked against all the SimpleConstraint names in model.
            self.__callback(self.__constraint)
            self.grab_release()
            self.destroy()
        except BGError as e:
            messagebox.showerror('Error', e.message, parent=self)
class ConstraintsTab(Tab, HasCommonSetup, SubscribesToEvents, Resetable):
    """Used to create, edit and remove constraints.

    Attributes:
        __selected_constraint: Currently selected constraint in the constraints list view.
    """
    def __init__(self, parent_notebook):

        self.__state = State()
        self.__selected_constraint: Optional[
            Any] = None  # can be either simple or complex constraint

        Tab.__init__(self, parent_notebook, TAB_NAME)
        HasCommonSetup.__init__(self)
        SubscribesToEvents.__init__(self)

    # HasCommonSetup
    def _create_widgets(self) -> None:
        self.__constraints_listbox = ScrollbarListbox(
            self,
            columns=[Column('Type')],
            heading='Constraint',
            extract_id=lambda crt: crt.id_,
            extract_text=lambda crt: crt.name,
            extract_values=lambda crt: 'Simple'
            if isinstance(crt, SimpleConstraint) else 'Complex',
            on_select_callback=self.__on_select_callback,
            values=self.__state.model.get_all_constraints())
        self.__right_frame = ttk.Frame(self)
        # Ctr label
        self.__ctr_label_var = tk.StringVar(value='')
        self.__ctr_label = ttk.Label(self.__right_frame,
                                     textvariable=self.__ctr_label_var,
                                     style='Big.TLabel',
                                     anchor=tk.CENTER)

        self.__add_simple_constraint_button = ttk.Button(
            self.__right_frame,
            text='Add simple constraint',
            command=self.__on_add)
        self.__add_complex_constraint_button = ttk.Button(
            self.__right_frame,
            text='Add complex constraint',
            command=lambda: self.__on_add(complex_=True))
        self.__edit_constraint_button = ttk.Button(self.__right_frame,
                                                   text='Edit',
                                                   state=tk.DISABLED,
                                                   command=self.__on_edit)
        self.__remove_constraint_button = ttk.Button(
            self.__right_frame,
            text='Remove',
            state=tk.DISABLED,
            command=self.__remove_constraint)

    def _setup_layout(self) -> None:
        self.__constraints_listbox.grid(row=0, column=0, sticky=tk.NSEW)
        self.__right_frame.grid(row=0,
                                column=1,
                                sticky=tk.NSEW,
                                pady=FRAME_PAD_Y,
                                padx=FRAME_PAD_X)

        self.__ctr_label.grid(row=0,
                              column=0,
                              pady=CONTROL_PAD_Y,
                              sticky=tk.EW)
        self.__add_simple_constraint_button.grid(row=1,
                                                 column=0,
                                                 sticky=tk.NSEW,
                                                 pady=CONTROL_PAD_Y)
        self.__add_complex_constraint_button.grid(row=2,
                                                  column=0,
                                                  sticky=tk.NSEW,
                                                  pady=CONTROL_PAD_Y)
        self.__edit_constraint_button.grid(row=3,
                                           column=0,
                                           sticky=tk.NSEW,
                                           pady=CONTROL_PAD_Y)
        self.__remove_constraint_button.grid(row=4,
                                             column=0,
                                             sticky=tk.NSEW,
                                             pady=CONTROL_PAD_Y)

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

    # SubscribesToEvents
    def _subscribe_to_events(self) -> None:
        pub.subscribe(self.__on_model_changed, actions.MODEL_LOADED)
        pub.subscribe(self.__on_model_changed, actions.TAXONOMY_EDITED)
        pub.subscribe(self._reset, actions.RESET)

    # Resetable
    def _reset(self) -> None:
        self.__constraints_listbox.set_items([])
        self.__selected_constraint = None
        self.__ctr_label_var.set('')
        change_controls_state(tk.DISABLED, self.__edit_constraint_button,
                              self.__remove_constraint_button)

    def __on_select_callback(self, ctr_id: int):
        """Executed whenever a constraints listview item is selected (by mouse click).

        :param ctr_id: Id of the selected constraint.
        """
        selected_ctr = self.__state.model.get_constraint(id_=ctr_id)
        if selected_ctr:
            self.__selected_constraint = selected_ctr
            self.__ctr_label_var.set(
                trim_string(selected_ctr.name, length=LABEL_LENGTH))
            # Enable buttons
            change_controls_state(tk.NORMAL, self.__edit_constraint_button,
                                  self.__remove_constraint_button)

    def __add(self, ctr: Any) -> None:
        """Executed after adding a new constraint in SimpleConstraintWindow or ComplexConstraintWindow.

        :param ctr: New SimpleConstraint or ComplexConstraint obtained from SimpleConstraintWindow
            or ComplexConstraintWindow.
        """
        _, index = self.__state.model.add_constraint(ctr)
        self.__constraints_listbox.add_item(ctr, index=index)
        # Set selected constraint to the newly created constraint
        self.__selected_constraint = ctr
        self.__ctr_label_var.set(trim_string(ctr.name, length=LABEL_LENGTH))
        # Enable buttons
        change_controls_state(tk.NORMAL, self.__edit_constraint_button,
                              self.__remove_constraint_button)

    def __edit(self, ctr: Any):
        """Executed after editing the constraint in SimpleConstraintWindow or ComplexConstraintWindow.

        :param ctr: Edited SimpleConstraint or ComplexConstraint obtained from SimpleConstraintWindow
            or ComplexConstraintWindow.
        """
        _, index = self.__state.model.edit_constraint(ctr)
        self.__selected_constraint = ctr
        # Just in case if the name has changed
        self.__constraints_listbox.rename_item(
            ctr, index=index)  # Update constraint name in treeview
        self.__ctr_label_var.set(trim_string(
            ctr.name, length=LABEL_LENGTH))  # Update constraint label

    def __on_add(self, complex_=False):
        """Executed whenever the __add_simple_constraint_button or __add_complex_constraint_button are pressed.

        :param complex_: If __add_complex_constraint_button is pressed then set to True; False otherwise.
        """
        if complex_:
            ComplexConstraintWindow(self, callback=self.__add)
        else:
            SimpleConstraintWindow(self, callback=self.__add)

    def __on_edit(self):
        """Executed whenever the __edit_constraint_button is pressed."""
        if self.__selected_constraint:
            if isinstance(self.__selected_constraint, SimpleConstraint):
                SimpleConstraintWindow(self,
                                       constraint=self.__selected_constraint,
                                       callback=self.__edit)
            else:
                ComplexConstraintWindow(self,
                                        constraint=self.__selected_constraint,
                                        callback=self.__edit)

    def __remove_constraint(self):
        """Executed whenever the __remove_constraint_button is pressed.
        Removes selected constraint from model.
        """
        if self.__selected_constraint:
            self.__state.model.remove_constraint(self.__selected_constraint)
            self.__constraints_listbox.remove_item_recursively(
                self.__selected_constraint)
            self.__selected_constraint = None
            # Hide widgets
            self.__ctr_label_var.set('')
            change_controls_state(tk.DISABLED, self.__edit_constraint_button,
                                  self.__remove_constraint_button)

    def __on_model_changed(self):
        """Executed whenever a model is loaded from file or taxonomy changes."""
        ctrs = self.__state.model.get_all_constraints()
        self.__constraints_listbox.set_items(ctrs)
        self.__selected_constraint = None
        self.__ctr_label_var.set('')
        change_controls_state(tk.DISABLED, self.__edit_constraint_button,
                              self.__remove_constraint_button)
class ComplexConstraintWindow(HasCommonSetup, Window):
    """Used to create/edit complex constraint.

    Attributes:
        __callback: Function to be executed when the 'OK' button is pressed, before destroying the window.
        __constraint: New constraint / deep copy of the edited constraint.
        __antecedent: List of SimpleConstraints forming an antecedent.
        __consequent: List of SimpleConstraints forming a consequent.
        __selected_antecedent: Currently selected simple constraint in the antecedent list view.
        __selected_consequent: Currently selected simple constraint in the consequent list view.
    """
    def __init__(self,
                 parent_frame,
                 callback: Callable[[Any], Any],
                 constraint: Optional[ComplexConstraint] = None):
        self.__callback = callback

        self.__constraint: ComplexConstraint = copy.deepcopy(constraint) if constraint is not None \
            else ComplexConstraint()

        self.__antecedent: List[
            SimpleConstraint] = [] if constraint is None else [
                copy.deepcopy(sc) for sc in constraint.antecedent
            ]
        self.__consequent: List[
            SimpleConstraint] = [] if constraint is None else [
                copy.deepcopy(sc) for sc in constraint.consequent
            ]
        self.__selected_antecedent: Optional[SimpleConstraint] = None
        self.__selected_consequent: Optional[SimpleConstraint] = None

        Window.__init__(self, parent_frame, WINDOW_TITLE)
        HasCommonSetup.__init__(self)

    # HasCommonSetup
    def _create_widgets(self) -> None:
        # Name
        self.__data_frame = ttk.Frame(self)
        self.__name_entry_var = tk.StringVar(value=self.__constraint.name)
        self.__name_entry_label = ttk.Label(self.__data_frame, text='Name:')
        self.__name_entry = ttk.Entry(self.__data_frame,
                                      textvariable=self.__name_entry_var)
        # Description
        self.__description_text_label = ttk.Label(self.__data_frame,
                                                  text='Description:')
        self.__description_text = tk.Text(self.__data_frame,
                                          height=8,
                                          width=40)
        if self.__constraint.description:
            self.__description_text.insert(tk.INSERT,
                                           self.__constraint.description)

        self.__implication_frame = ttk.Frame(self)

        self.__antecedent_frame = ttk.Frame(self.__implication_frame)
        self.__consequent_frame = ttk.Frame(self.__implication_frame)

        self.__antecedent_listbox = ScrollbarListbox(
            self.__antecedent_frame,
            values=self.__antecedent,
            heading='Condition',
            extract_id=lambda ctr: ctr.id_,
            extract_text=lambda ctr: ctr.name,
            on_select_callback=self.__on_select_antecedent)
        self.__consequent_listbox = ScrollbarListbox(
            self.__consequent_frame,
            values=self.__consequent,
            heading='Consequence',
            extract_id=lambda ctr: ctr.id_,
            extract_text=lambda ctr: ctr.name,
            on_select_callback=self.__on_select_consequent)

        # Antecedent all/any
        self.__antecedent_all_var = tk.BooleanVar(
            value=self.__constraint.antecedent_all)
        self.__antecedent_all_radiobutton = ttk.Radiobutton(
            self.__antecedent_frame,
            text='All',
            value=True,
            variable=self.__antecedent_all_var)
        self.__antecedent_any_radiobutton = ttk.Radiobutton(
            self.__antecedent_frame,
            text='Any',
            value=False,
            variable=self.__antecedent_all_var)

        # Consequent all/any
        self.__consequent_all_var = tk.BooleanVar(
            value=self.__constraint.consequent_all)
        self.__consequent_all_radiobutton = ttk.Radiobutton(
            self.__consequent_frame,
            text='All',
            value=True,
            variable=self.__consequent_all_var)
        self.__consequent_any_radiobutton = ttk.Radiobutton(
            self.__consequent_frame,
            text='Any',
            value=False,
            variable=self.__consequent_all_var)

        self.__add_antecedent_button = ttk.Button(
            self.__antecedent_frame,
            text='Add',
            command=self.__on_add_antecedent)
        self.__edit_antecedent_button = ttk.Button(
            self.__antecedent_frame,
            text='Edit',
            command=self.__on_edit_antecedent,
            state=tk.DISABLED)
        self.__remove_antecedent_button = ttk.Button(
            self.__antecedent_frame,
            text='Remove',
            state=tk.DISABLED,
            command=self.__remove_antecedent)

        self.__add_consequent_button = ttk.Button(
            self.__consequent_frame,
            text='Add',
            command=self.__on_add_consequent)
        self.__edit_consequent_button = ttk.Button(
            self.__consequent_frame,
            text='Edit',
            command=self.__on_edit_consequent,
            state=tk.DISABLED)
        self.__remove_consequent_button = ttk.Button(
            self.__consequent_frame,
            text='Remove',
            state=tk.DISABLED,
            command=self.__remove_consequent)

        # Buttons frame
        self.__buttons_frame = ttk.Frame(self)
        self.__ok_button = ttk.Button(self.__buttons_frame,
                                      text='Ok',
                                      command=self.__ok)
        self.__cancel_button = ttk.Button(self.__buttons_frame,
                                          text='Cancel',
                                          command=self.destroy)

    def _setup_layout(self) -> None:
        self.__data_frame.grid(row=0,
                               column=0,
                               sticky=tk.NSEW,
                               padx=FRAME_PAD_X,
                               pady=FRAME_PAD_Y)
        self.__name_entry_label.grid(row=0,
                                     column=0,
                                     sticky=tk.W,
                                     pady=CONTROL_PAD_Y)
        self.__name_entry.grid(row=0,
                               column=1,
                               sticky=tk.EW,
                               pady=CONTROL_PAD_Y)
        self.__description_text_label.grid(row=1,
                                           column=0,
                                           sticky=tk.W,
                                           pady=CONTROL_PAD_Y)
        self.__description_text.grid(row=1,
                                     column=1,
                                     sticky=tk.EW,
                                     pady=CONTROL_PAD_Y)

        self.__implication_frame.grid(row=1, column=0, sticky=tk.NSEW)
        self.__antecedent_frame.grid(row=0,
                                     column=0,
                                     sticky=tk.NSEW,
                                     padx=FRAME_PAD_X,
                                     pady=FRAME_PAD_Y)
        self.__consequent_frame.grid(row=0,
                                     column=1,
                                     sticky=tk.NSEW,
                                     padx=FRAME_PAD_X,
                                     pady=FRAME_PAD_Y)

        self.__antecedent_listbox.grid(row=0,
                                       column=0,
                                       columnspan=4,
                                       sticky=tk.NSEW)
        self.__antecedent_all_radiobutton.grid(row=1,
                                               column=1,
                                               sticky=tk.W,
                                               pady=CONTROL_PAD_Y)
        self.__antecedent_any_radiobutton.grid(row=1,
                                               column=2,
                                               sticky=tk.W,
                                               pady=CONTROL_PAD_Y)
        self.__add_antecedent_button.grid(row=2,
                                          column=1,
                                          columnspan=2,
                                          pady=CONTROL_PAD_Y,
                                          sticky=tk.NSEW)
        self.__edit_antecedent_button.grid(row=3,
                                           column=1,
                                           columnspan=2,
                                           pady=CONTROL_PAD_Y,
                                           sticky=tk.NSEW)
        self.__remove_antecedent_button.grid(row=4,
                                             column=1,
                                             columnspan=2,
                                             pady=CONTROL_PAD_Y,
                                             sticky=tk.NSEW)

        self.__consequent_listbox.grid(row=0,
                                       column=0,
                                       columnspan=4,
                                       sticky=tk.NSEW)
        self.__consequent_all_radiobutton.grid(row=1,
                                               column=1,
                                               sticky=tk.W,
                                               pady=CONTROL_PAD_Y)
        self.__consequent_any_radiobutton.grid(row=1,
                                               column=2,
                                               sticky=tk.W,
                                               pady=CONTROL_PAD_Y)
        self.__add_consequent_button.grid(row=2,
                                          column=1,
                                          columnspan=2,
                                          pady=CONTROL_PAD_Y,
                                          sticky=tk.NSEW)
        self.__edit_consequent_button.grid(row=3,
                                           column=1,
                                           columnspan=2,
                                           pady=CONTROL_PAD_Y,
                                           sticky=tk.NSEW)
        self.__remove_consequent_button.grid(row=4,
                                             column=1,
                                             columnspan=2,
                                             pady=CONTROL_PAD_Y,
                                             sticky=tk.NSEW)

        self.__buttons_frame.grid(row=2,
                                  column=0,
                                  sticky=tk.NSEW,
                                  padx=FRAME_PAD_X,
                                  pady=FRAME_PAD_Y)
        self.__ok_button.grid(row=0, column=0, sticky=tk.E)
        self.__cancel_button.grid(row=0, column=1, sticky=tk.W)

        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)
        self.__implication_frame.columnconfigure(0, weight=1, uniform='fred')
        self.__implication_frame.columnconfigure(1, weight=1, uniform='fred')

        self.__implication_frame.rowconfigure(0, weight=1, uniform='fred')

        self.__antecedent_frame.rowconfigure(0, weight=1)
        self.__antecedent_frame.columnconfigure(0, weight=1)
        self.__antecedent_frame.columnconfigure(1, weight=1)
        self.__antecedent_frame.columnconfigure(2, weight=1)
        self.__antecedent_frame.columnconfigure(3, weight=1)
        self.__consequent_frame.rowconfigure(0, weight=1)
        self.__consequent_frame.columnconfigure(0, weight=1)
        self.__consequent_frame.columnconfigure(1, weight=1)
        self.__consequent_frame.columnconfigure(2, weight=1)
        self.__consequent_frame.columnconfigure(3, weight=1)

        self.__buttons_frame.columnconfigure(0, weight=1)
        self.__buttons_frame.columnconfigure(1, weight=1)

        self._set_geometry(width_ratio=WINDOW_WIDTH_RATIO,
                           height_ratio=WINDOW_HEIGHT_RATIO)

    def __ok(self):
        """Executed whenever the __ok_button is pressed. Rewrites the information from window to the constraint object,
        calls the callback on it and destroys the window."""
        try:
            self.__constraint.name = self.__name_entry_var.get()
            self.__constraint.description = self.__description_text.get(
                1.0, tk.END)
            self.__constraint.antecedent = self.__antecedent
            self.__constraint.antecedent_all = self.__antecedent_all_var.get()
            self.__constraint.consequent = self.__consequent
            self.__constraint.consequent_all = self.__consequent_all_var.get()

            self.__callback(self.__constraint)
            self.grab_release()
            self.destroy()
        except BGError as e:
            messagebox.showerror('Error', e.message, parent=self)

    def __on_select_antecedent(self, id_: int) -> None:
        """Executed whenever a __antecedent_listbox item is selected (by mouse click).

        :param id_: Id of the selected antecedent item.
        """
        self.__selected_antecedent = next(
            (a for a in self.__antecedent if a.id_ == id_), None)
        # Enable widgets
        change_controls_state(tk.NORMAL, self.__edit_antecedent_button,
                              self.__remove_antecedent_button)

    def __on_select_consequent(self, id_: int) -> None:
        """Executed whenever a __consequent_listbox item is selected (by mouse click).

        :param id_: Id of the selected consequent item.
        """
        self.__selected_consequent = next(
            (c for c in self.__consequent if c.id_ == id_), None)
        # Enable widgets
        change_controls_state(tk.NORMAL, self.__edit_consequent_button,
                              self.__remove_consequent_button)

    def __on_add_antecedent(self) -> None:
        """Executed whenever the __add_antecedent_button is pressed."""
        SimpleConstraintWindow(self, callback=self.__add_antecedent)

    def __add_antecedent(self, ant: SimpleConstraint) -> None:
        """Executed whenever an item is added to antecedent.

        :param ant: Item to add to antecedent.
        """
        ant, index = self.__validate_constraint(ant,
                                                antecedent=True,
                                                added=True)
        self.__antecedent.append(ant)
        self.__antecedent_listbox.add_item(ant, index=index)
        self.__selected_antecedent = ant
        # Enable controls
        change_controls_state(tk.NORMAL, self.__edit_antecedent_button,
                              self.__remove_antecedent_button)

    def __on_edit_antecedent(self) -> None:
        """Executed whenever the __edit_antecedent_button is pressed."""
        if self.__selected_antecedent:
            SimpleConstraintWindow(self,
                                   constraint=self.__selected_antecedent,
                                   callback=self.__edit_antecedent)

    def __edit_antecedent(self, ant: SimpleConstraint) -> None:
        """Executed whenever an item from antecedent is edited.

        :param ant: Edited item from antecedent.
        """
        ant, index = self.__validate_constraint(ant,
                                                antecedent=True,
                                                added=False)
        self.__antecedent_listbox.rename_item(ant, index=index)
        # Replace selected antecedent with edited
        self.__antecedent.remove(self.__selected_antecedent)
        self.__antecedent.append(ant)
        self.__antecedent_listbox.select_item(ant)
        self.__selected_antecedent = ant

    def __remove_antecedent(self) -> None:
        """Executes whenever __remove_antecedent_button is pressed. Removes item from antecedent."""
        if self.__selected_antecedent:
            self.__antecedent.remove(self.__selected_antecedent)
            self.__antecedent_listbox.remove_item_recursively(
                self.__selected_antecedent)
            self.__selected_antecedent = None
            # Disable widgets
            change_controls_state(tk.DISABLED, self.__edit_antecedent_button,
                                  self.__remove_antecedent_button)

    def __on_add_consequent(self) -> None:
        """Executed whenever the __add_consequent_button is pressed."""
        SimpleConstraintWindow(self, callback=self.__add_consequent)

    def __add_consequent(self, con: SimpleConstraint) -> None:
        """Executed whenever an item is added to consequent.

        :param con: Item to add to consequent.
        """
        con, index = self.__validate_constraint(con,
                                                antecedent=False,
                                                added=True)
        self.__consequent.append(con)
        self.__consequent_listbox.add_item(con, index=index)
        self.__selected_consequent = con
        # Enable controls
        change_controls_state(tk.NORMAL, self.__edit_consequent_button,
                              self.__remove_consequent_button)

    def __on_edit_consequent(self) -> None:
        """Executed whenever the __edit_consequent_button is pressed."""
        if self.__selected_consequent:
            SimpleConstraintWindow(self,
                                   constraint=self.__selected_consequent,
                                   callback=self.__edit_consequent)

    def __edit_consequent(self, con: SimpleConstraint) -> None:
        """Executed whenever an item from consequent is edited.

        :param con: Edited item from consequent.
        """
        con, index = self.__validate_constraint(con,
                                                antecedent=False,
                                                added=True)
        self.__consequent_listbox.rename_item(con, index=index)
        # Replace selected antecedent with edited
        self.__consequent.remove(self.__selected_consequent)
        self.__consequent.append(con)
        self.__consequent_listbox.select_item(con)
        self.__selected_consequent = con

    def __remove_consequent(self) -> None:
        if self.__selected_consequent:
            self.__consequent.remove(self.__selected_consequent)
            self.__consequent_listbox.remove_item_recursively(
                self.__selected_consequent)
            self.__selected_consequent = None
            change_controls_state(tk.DISABLED, self.__edit_consequent_button,
                                  self.__remove_consequent_button)

    @staticmethod
    def __get_element_index(str_list: List[str], item: str) -> int:
        """Returns an index of an name on a list. If element is not present on the list, it gets added to it.

        :param str_list: List of strings.
        :param item: Item.
        :return: Index of the item on the list.
        """
        if item not in str_list:
            str_list.append(item)
        return sorted(str_list).index(item)

    def __validate_constraint(self, ctr: SimpleConstraint, antecedent: bool, added: bool) -> \
            Tuple[SimpleConstraint, int]:
        """Validates an item of either antecedent or consequent and normalizes it's name.

        :param ctr: item.
        :param antecedent: If True then the item is a part of antecedent; Otherwise, a part of consequent.
        :param added: If True then item is added; Otherwise edited.
        :return: Tuple of the form (validate item, its index in the list of antecedents or consequents)
        """
        ctr.name = normalize_name(ctr.name)
        selected_ctr = None if added else self.__selected_antecedent if antecedent else self.__selected_consequent
        names = [a.name for a in self.__antecedent
                 ] if antecedent else [c.name for c in self.__consequent]
        if ctr.name in names and not (selected_ctr is not None
                                      and selected_ctr.id_ == ctr.id_):
            # If the name is contained in "names", and it's not because the constraint is edited
            ctr_type_string = 'Antecedent' if antecedent else 'Consequent'
            raise BGError(
                f'{ctr_type_string} already exists in complex constraint.')

        if not ctr.components_ids:
            raise BGError('Constraint must contain components.')
        elif ctr.max_ is None and ctr.min_ is None:
            raise BGError('Constraint has to have at least 1 bound.')

        return ctr, self.__get_element_index(names, ctr.name)
示例#8
0
class ResourcesTab(Tab, HasCommonSetup, SubscribesToEvents, Resetable):
    """Used to create, edit and remove components.

    Attributes:
        __selected_resource: Currently selected resource in the resources listview.
        __selected_component: Currently selected component in the components taxonomy view.
    """
    def __init__(self, parent_notebook):
        self.__state = State()
        self.__selected_resource: Optional[Resource] = None
        self.__selected_component: Optional[Component] = None

        Tab.__init__(self, parent_notebook, TAB_NAME)
        HasCommonSetup.__init__(self)
        SubscribesToEvents.__init__(self)

    # HasCommonSetup
    def _create_widgets(self):
        self.__taxonomy_tree = ScrollbarListbox(
            self,
            on_select_callback=self.__on_select_tree_item,
            heading=TREEVIEW_HEADING,
            extract_id=lambda x: x.id_,
            extract_text=lambda x: x.name,
            extract_ancestor=lambda x: ''
            if x.parent_id is None else x.parent_id,
            extract_values=self.__extract_values,
            columns=[Column('Produces')],
            values=self.__state.model.taxonomy)
        self.__left_frame = ttk.Frame(self)
        # Resources combobox
        self.__resource_combobox_var = tk.StringVar(value=SELECT_RESOURCE)
        self.__resource_combobox_var.trace('w', self.__on_combobox_changed)
        self.__resource_combobox = ttk.Combobox(
            self.__left_frame,
            state='readonly',
            font=FONT,
            textvariable=self.__resource_combobox_var)
        # Fill the Combobox
        resources_names = self.__state.model.get_all_resources_names()
        self.__resource_combobox['values'] = sorted(resources_names)
        self.__resource_combobox_var.set(SELECT_RESOURCE)
        # C(r)ud Buttons
        self.__add_resource_button = ttk.Button(self.__left_frame,
                                                text='Add',
                                                state=tk.NORMAL,
                                                command=self.__on_add)
        self.__rename_resource_button = ttk.Button(self.__left_frame,
                                                   text='Rename',
                                                   state=tk.DISABLED,
                                                   command=self.__on_rename)
        self.__remove_resource_button = ttk.Button(self.__left_frame,
                                                   text='Remove',
                                                   state=tk.DISABLED,
                                                   command=self.__remove)
        # Cmp label
        self.__cmp_label_var = tk.StringVar(value='')
        self.__cmp_label = ttk.Label(self.__left_frame,
                                     textvariable=self.__cmp_label_var,
                                     style='Big.TLabel',
                                     anchor=tk.CENTER)
        self.__produces_spinbox_label = ttk.Label(self.__left_frame,
                                                  text='Produces:')
        self.__produces_spinbox_var = tk.IntVar(value='')
        self.__produces_spinbox_var.trace('w', self.__on_produced_changed)
        self.__produces_spinbox = ttk.Spinbox(
            self.__left_frame,
            from_=-math.inf,
            to=math.inf,
            textvariable=self.__produces_spinbox_var)

        self.__all_children_produce_spinbox_label = ttk.Label(
            self.__left_frame, text='Produces:')
        self.__all_children_produce_spinbox_var = tk.IntVar(value=0)
        self.__all_children_produce_spinbox = ttk.Spinbox(
            self.__left_frame,
            from_=-math.inf,
            to=math.inf,
            textvariable=self.__all_children_produce_spinbox_var)
        self.__apply_to_all_children_button = ttk.Button(
            self.__left_frame,
            text='Apply to all children',
            command=self.__apply_to_all_children)

    def _setup_layout(self):
        self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)

        self.__left_frame.grid(row=0,
                               column=0,
                               sticky=tk.NSEW,
                               pady=FRAME_PAD_Y,
                               padx=FRAME_PAD_X)

        self.__resource_combobox.grid(row=0,
                                      column=0,
                                      columnspan=2,
                                      sticky=tk.NSEW,
                                      pady=CONTROL_PAD_Y)

        self.__add_resource_button.grid(row=1,
                                        column=0,
                                        columnspan=2,
                                        sticky=tk.NSEW,
                                        pady=CONTROL_PAD_Y)
        self.__rename_resource_button.grid(row=2,
                                           column=0,
                                           columnspan=2,
                                           sticky=tk.NSEW,
                                           pady=CONTROL_PAD_Y)
        self.__remove_resource_button.grid(row=3,
                                           column=0,
                                           columnspan=2,
                                           sticky=tk.NSEW,
                                           pady=CONTROL_PAD_Y)

        self.__cmp_label.grid(row=4,
                              column=0,
                              columnspan=2,
                              pady=CONTROL_PAD_Y,
                              sticky=tk.EW)

        self.__produces_spinbox_label.grid(row=5,
                                           column=0,
                                           pady=CONTROL_PAD_Y,
                                           sticky=tk.W)
        self.__produces_spinbox.grid(row=5,
                                     column=1,
                                     pady=CONTROL_PAD_Y,
                                     sticky=tk.NSEW)

        self.__all_children_produce_spinbox_label.grid(row=5,
                                                       column=0,
                                                       pady=CONTROL_PAD_Y,
                                                       sticky=tk.W)
        self.__all_children_produce_spinbox.grid(row=5,
                                                 column=1,
                                                 pady=CONTROL_PAD_Y,
                                                 sticky=tk.NSEW)
        self.__apply_to_all_children_button.grid(row=6,
                                                 column=0,
                                                 columnspan=2,
                                                 sticky=tk.NSEW,
                                                 pady=CONTROL_PAD_Y)

        self.columnconfigure(0, weight=1, uniform='fred')
        self.columnconfigure(1, weight=3, uniform='fred')
        self.rowconfigure(0, weight=1)

        self.__left_frame.columnconfigure(1, weight=1)

        # Hide widgets
        self.__taxonomy_tree.grid_forget()
        self.__cmp_label.grid_forget()
        self.__produces_spinbox_label.grid_forget()
        self.__produces_spinbox.grid_forget()
        self.__all_children_produce_spinbox_label.grid_forget()
        self.__all_children_produce_spinbox.grid_forget()
        self.__apply_to_all_children_button.grid_forget()

    # SubscribesToListeners
    def _subscribe_to_events(self):
        pub.subscribe(self.__on_model_loaded, actions.MODEL_LOADED)
        pub.subscribe(self.__on_taxonomy_edited, actions.TAXONOMY_EDITED)
        pub.subscribe(self._reset, actions.RESET)

    # HasTaxonomyTree
    def __on_select_tree_item(self, cmp_id: int) -> None:
        """Executed whenever a tree item is selected (by mouse click).

        :param cmp_id: Id of the selected component.
        """
        if self.__selected_resource:
            selected_cmp: Component = self.__state.model.get_component(
                id_=cmp_id)
            self.__selected_component = selected_cmp

            self.__cmp_label.grid(row=4,
                                  column=0,
                                  columnspan=2,
                                  pady=CONTROL_PAD_Y,
                                  sticky=tk.EW)
            self.__cmp_label_var.set(trim_string(selected_cmp.name, length=23))

            if selected_cmp.is_leaf:
                self.__all_children_produce_spinbox_label.grid_forget(
                )  # Hide widgets (changing all children)
                self.__all_children_produce_spinbox.grid_forget()
                self.__apply_to_all_children_button.grid_forget()
                produced = 0
                if self.__selected_resource.id_ in selected_cmp.produces:
                    produced = selected_cmp.produces[
                        self.__selected_resource.id_]
                self.__produces_spinbox_var.set(produced)
                self.__produces_spinbox_label.grid(row=5,
                                                   column=0,
                                                   pady=CONTROL_PAD_Y,
                                                   sticky=tk.W)
                self.__produces_spinbox.grid(row=5,
                                             column=1,
                                             pady=CONTROL_PAD_Y,
                                             sticky=tk.NSEW)
            else:
                self.__produces_spinbox_label.grid_forget(
                )  # Hide widgets for leaves
                self.__produces_spinbox.grid_forget()
                # Show widgets (changing all children)
                self.__all_children_produce_spinbox_var.set(0)
                self.__all_children_produce_spinbox_label.grid(
                    row=5, column=0, pady=CONTROL_PAD_Y, sticky=tk.W)
                self.__all_children_produce_spinbox.grid(row=5,
                                                         column=1,
                                                         pady=CONTROL_PAD_Y,
                                                         sticky=tk.NSEW)
                self.__apply_to_all_children_button.grid(row=6,
                                                         column=0,
                                                         columnspan=2,
                                                         sticky=tk.NSEW,
                                                         pady=CONTROL_PAD_Y)

    def __extract_values(self, cmp: Component) -> Tuple[Any, ...]:
        """Extracts the data of the component to show in the taxonomy view.

        :param cmp: Component from which to extract the data.
        :return: Tuple containing data about component
            (amount of produced resource of type __selected_resource,).
        """
        produces = ''
        if self.__selected_resource:
            if self.__selected_resource.id_ in cmp.produces:
                produces = cmp.produces[self.__selected_resource.id_]
        return produces,  # Coma means 1-element tuple

    def __build_tree(self) -> None:
        """Fills the tree view with components from model."""
        self.__taxonomy_tree.set_items(self.__state.model.taxonomy)

    def __on_model_loaded(self):
        """Executed whenever a model is loaded from file."""
        self._reset()
        self.__build_tree()
        resources_names = self.__state.model.get_all_resources_names()
        self.__resource_combobox['values'] = sorted(resources_names)
        self.__resource_combobox_var.set(SELECT_RESOURCE)

    def __on_taxonomy_edited(self) -> None:
        """Executed whenever the structure of the taxonomy changes."""
        self._reset()
        self.__build_tree()

    # Resetable
    def _reset(self) -> None:
        self.__selected_component = None
        self.__selected_resource = None
        # Hide widgets
        self.__produces_spinbox_label.grid_forget()
        self.__produces_spinbox.grid_forget()
        self.__all_children_produce_spinbox_label.grid_forget()
        self.__all_children_produce_spinbox.grid_forget()
        self.__apply_to_all_children_button.grid_forget()
        # Set entries to default
        self.__resource_combobox['values'] = ()
        self.__resource_combobox_var.set(SELECT_RESOURCE)
        self.__produces_spinbox_var.set('')
        self.__all_children_produce_spinbox_var.set('')
        self.__cmp_label_var.set('')
        # Disable buttons
        change_controls_state(tk.DISABLED, self.__rename_resource_button,
                              self.__remove_resource_button)
        self.__taxonomy_tree.set_items([])
        self.__taxonomy_tree.grid_forget()

    # Class-specific
    def __on_combobox_changed(self, *_):
        """Executed whenever the __resource_combobox value changes."""
        res_name = self.__resource_combobox_var.get()
        resource = self.__state.model.get_resource(name=res_name)
        if resource:
            # Enable buttons
            change_controls_state(tk.NORMAL, self.__rename_resource_button,
                                  self.__remove_resource_button)
            self.__selected_resource = resource
            if self.__selected_component:
                if self.__selected_component.is_leaf:
                    produced = 0
                    if resource.id_ in self.__selected_component.produces:
                        produced = self.__selected_component.produces[
                            self.__selected_resource.id_]
                    self.__produces_spinbox_var.set(produced)
                else:
                    self.__all_children_produce_spinbox_var.set('')

            # Show the taxonomy tree
            self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)
            self.__update_tree()

    def __update_tree(self):
        """Updates every leaf component in the taxonomy treeview."""
        leaf_cmps = self.__state.model.get_components(is_leaf=True)
        self.__taxonomy_tree.update_values(*leaf_cmps)

    def __on_add(self):
        """Executed whenever the __add_resource_button is pressed."""
        AskStringWindow(self, self.__add, 'Add resource',
                        'Enter name of a new resource.')

    def __add(self, name: str):
        """Executed after creating of a new resource.

        :param name: Name of a new resource.
        """
        res = Resource(name)
        self.__state.model.add_resource(res)
        self.__selected_resource = res
        self.__resource_combobox['values'] = sorted(
            self.__state.model.get_all_resources_names())
        self.__resource_combobox_var.set(res.name)
        # Enable buttons
        change_controls_state(tk.NORMAL, self.__rename_resource_button,
                              self.__remove_resource_button)
        # Show the taxonomy tree
        self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)
        self.__update_tree()

    def __on_rename(self) -> None:
        """Executed whenever the __rename_resource_button is pressed."""
        if self.__selected_resource:
            AskStringWindow(
                self,
                self.__rename,
                'Rename resource',
                f'Enter new name for "{self.__selected_resource.name}" resource.',
                string=self.__selected_resource.name)

    def __rename(self, new_name: str) -> None:
        """Executed after renaming of a resource.

        :param new_name: New name for the __selected_resource.
        """
        res = self.__state.model.rename_resource(self.__selected_resource,
                                                 new_name)
        self.__resource_combobox['values'] = sorted(
            self.__state.model.get_all_resources_names())
        self.__resource_combobox_var.set(res.name)

    def __remove(self):
        """Executed whenever the __remove_resource_button is pressed.
        Removes __selected_resource from model.
        """
        if self.__selected_resource:
            self.__state.model.remove_resource(self.__selected_resource)
            self.__resource_combobox['values'] = sorted(
                self.__state.model.get_all_resources_names())
            self.__selected_resource = None

            self.__resource_combobox_var.set(SELECT_RESOURCE)
            # Disable buttons
            change_controls_state(tk.DISABLED, self.__rename_resource_button,
                                  self.__remove_resource_button)
            # Hide widgets
            self.__cmp_label.grid_forget()
            self.__produces_spinbox_label.grid_forget()
            self.__produces_spinbox.grid_forget()
            self.__all_children_produce_spinbox_label.grid_forget()
            self.__all_children_produce_spinbox.grid_forget()
            self.__apply_to_all_children_button.grid_forget()
            # Hide the taxonomy tree
            self.__taxonomy_tree.grid_forget()

    def __on_produced_changed(self, *_):
        """Executed whenever the __produces_spinbox value changes."""
        if self.__selected_component and self.__selected_resource:
            value = None
            try:
                value = self.__produces_spinbox_var.get()
            except tk.TclError:
                pass
            finally:
                if value:
                    self.__selected_component.produces[
                        self.__selected_resource.id_] = value
                elif self.__selected_resource.id_ in self.__selected_component.produces:
                    del self.__selected_component.produces[
                        self.__selected_resource.id_]

                self.__taxonomy_tree.update_values(self.__selected_component)

    def __apply_to_all_children(self):
        """Executed whenever the __apply_to_all_children_button is pressed."""
        if self.__selected_component and self.__selected_resource:
            value = None
            try:
                value = self.__all_children_produce_spinbox_var.get()
            except tk.TclError:
                pass
            finally:
                updated_components = self.__state.model.set_resource_production_to_all_components_children(
                    self.__selected_component, self.__selected_resource, value)
                self.__taxonomy_tree.update_values(*updated_components)
class InitialWindow(HasCommonSetup, Window):
    """Presents the initial window right after opening the program.

    Attributes:
        __callback: Callback function executed when new project is created / project is loaded from file,
            before destroying this window.
        __has_recent_projects: Evaluates to True if __settings contain recently_opened_projects;
            otherwise evaluates to False. Indicates whether to show the "Recently opened projects" listbox.
    """
    def __init__(self, parent_frame, callback: Callable):
        self.__state = State()
        self.__settings = Settings.get_settings()

        self.__callback = callback
        self.__has_recent_projects = len(
            self.__settings.recently_opened_projects) > 0

        Window.__init__(self, parent_frame, WINDOW_TITLE)
        HasCommonSetup.__init__(self)

    def _create_widgets(self) -> None:
        self.__main_frame = ttk.Frame(self)
        self.__labels_frame = ttk.Frame(self.__main_frame)
        self.__main_label = ttk.Label(self.__labels_frame,
                                      text=PROJECT_NAME,
                                      anchor=tk.CENTER,
                                      style='Big.TLabel')
        self.__secondary_label = ttk.Label(self.__labels_frame,
                                           text=PROJECT_DESCRIPTION,
                                           anchor=tk.CENTER)
        self.__version_label = ttk.Label(
            self.__labels_frame,
            text=f'{VERSION_STRING} {PROJECT_VERSION}',
            style='Small.TLabel',
            anchor=tk.CENTER)

        self.__create_new_project_button = ttk.Button(
            self.__main_frame,
            text='Create new project',
            command=self.__on_create_new_project)
        self.__open_project_button = ttk.Button(
            self.__main_frame,
            text='Open project',
            command=lambda: open_project(callback=self.__proceed))
        self.__solve_button = ttk.Button(self.__main_frame,
                                         text='Solve...',
                                         command=self.__on_solve)

        if self.__has_recent_projects:
            self.__recent_projects_listbox = ScrollbarListbox(
                self,
                on_select_callback=self.__on_select_recent_project,
                heading=TREEVIEW_HEADING,
                extract_id=lambda x: self.__settings.recently_opened_projects.
                index(x),
                extract_text=lambda x: f'{x.root_name} '
                f'(~/{extract_file_name(x.path)})',
                values=self.__settings.recently_opened_projects,
                has_scrollbars=False)

    def _setup_layout(self) -> None:
        self.__labels_frame.grid(row=0, sticky=tk.NSEW)
        self.__main_label.grid(row=0, sticky=tk.EW + tk.S, pady=CONTROL_PAD_Y)
        self.__secondary_label.grid(row=1, sticky=tk.EW, pady=CONTROL_PAD_Y)
        self.__version_label.grid(row=2,
                                  sticky=tk.EW + tk.N,
                                  pady=CONTROL_PAD_Y)

        self.__labels_frame.rowconfigure(0, weight=1)
        self.__labels_frame.rowconfigure(2, weight=1)
        self.__labels_frame.columnconfigure(0, weight=1)

        self.__create_new_project_button.grid(row=1,
                                              sticky=tk.EW,
                                              pady=CONTROL_PAD_Y)
        self.__open_project_button.grid(row=2,
                                        sticky=tk.EW,
                                        pady=CONTROL_PAD_Y)
        self.__solve_button.grid(row=3, sticky=tk.EW, pady=CONTROL_PAD_Y)

        if self.__has_recent_projects:
            self.__recent_projects_listbox.grid(row=0,
                                                column=0,
                                                sticky=tk.NSEW,
                                                pady=FRAME_PAD_Y,
                                                padx=FRAME_PAD_X)
            self.__main_frame.grid(row=0,
                                   column=1,
                                   sticky=tk.NSEW,
                                   pady=FRAME_PAD_Y,
                                   padx=FRAME_PAD_X)

            self.columnconfigure(0, weight=1)
            self.columnconfigure(1, weight=1)
        else:
            self.__main_frame.grid(row=0,
                                   column=0,
                                   sticky=tk.NSEW,
                                   pady=FRAME_PAD_Y,
                                   padx=FRAME_PAD_X)
            self.columnconfigure(0, weight=1)

        self.__main_frame.rowconfigure(0, weight=1)
        self.__main_frame.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self._set_geometry(height=WINDOW_HEIGHT,
                           width_ratio=WINDOW_WIDTH_RATIO)

    def __on_select_recent_project(self, project_index: int) -> None:
        """Executed whenever a project is selected from the list of recently opened projects.

        :param project_index: Index of the project in the list.
        """
        project_info = self.__settings.recently_opened_projects[project_index]
        load_from_file(project_info.path, callback=self.__proceed)
        # self.__open_project(project_info.path)

    def __create_new_project(self, root_name: str) -> None:
        """Creates new project with given root component name."""
        self.__state.model = Model()
        self.__state.model.set_root_name(root_name)
        self.__callback()
        self.destroy()

    def __on_create_new_project(self) -> None:
        """Executed whenever __create_new_project_button is pressed."""
        AskStringWindow(self,
                        self.__create_new_project,
                        window_title='Set root name',
                        prompt_text='Enter name of the root component')

    def __on_solve(self) -> None:
        """Executed whenever __create_new_project_button is pressed."""
        SolveWindow(self.__main_frame)

    def __proceed(self) -> None:
        """Executes callback and destroys initial window."""
        self.__callback()
        self.destroy()
class AssociationsTab(Tab, HasCommonSetup, SubscribesToEvents, Resetable):
    """Used to set the number of information about the associations between the root component and other components.

    Attributes:
        __selected_component: Currently selected component in the components taxonomy view.
    """
    def __init__(self, parent_notebook):
        self.__state = State()
        self.__selected_component: Optional[Component] = None

        Tab.__init__(self, parent_notebook, TAB_NAME)
        HasCommonSetup.__init__(self)
        SubscribesToEvents.__init__(self)

    # HasCommonSetup
    def _create_widgets(self):
        self.__taxonomy_tree = ScrollbarListbox(
            self,
            on_select_callback=self.__on_select_tree_item,
            heading=TREEVIEW_HEADING,
            extract_id=lambda x: x.id_,
            extract_text=lambda x: x.name,
            extract_ancestor=lambda x: ''
            if x.parent_id is None else x.parent_id,
            extract_values=self.__extract_values,
            values=self.__state.model.taxonomy,
            columns=[Column('Has association?'),
                     Column('Min'),
                     Column('Max')])

        self.__left_frame = ttk.Frame(self)
        # Cmp label
        self.__cmp_label_var = tk.StringVar(value='')
        self.__cmp_label = ttk.Label(self.__left_frame,
                                     textvariable=self.__cmp_label_var,
                                     anchor=tk.CENTER,
                                     style='Big.TLabel')
        # Has association checkbox
        self.__has_association_checkbox_var = tk.BooleanVar(value=False)
        self.__has_association_checkbox_var.trace(
            'w', self.__on_has_association_changed)
        self.__has_association_checkbox_label = ttk.Label(
            self.__left_frame, text='Has association?')
        self.__has_association_checkbox = ttk.Checkbutton(
            self.__left_frame,
            state=tk.DISABLED,
            variable=self.__has_association_checkbox_var)
        # Has min checkbox
        self.__has_min_checkbox_var = tk.BooleanVar(value=False)
        self.__has_min_checkbox_var.trace('w', self.__on_has_min_changed)
        self.__has_min_checkbox_label = ttk.Label(self.__left_frame,
                                                  text='Has min?')
        self.__has_min_checkbox = ttk.Checkbutton(
            self.__left_frame,
            state=tk.DISABLED,
            variable=self.__has_min_checkbox_var)
        # Min spinbox
        self.__min_spinbox_var = tk.IntVar(value='')
        self.__min_spinbox_var.trace('w', self.__on_min_changed)
        self.__min_spinbox = ttk.Spinbox(self.__left_frame,
                                         from_=0,
                                         to=math.inf,
                                         state=tk.DISABLED,
                                         textvariable=self.__min_spinbox_var,
                                         font=FONT)
        # Has max checkbox
        self.__has_max_checkbox_var = tk.BooleanVar(value=False)
        self.__has_max_checkbox_var.trace('w', self.__on_has_max_changed)
        self.__has_max_checkbox_label = ttk.Label(self.__left_frame,
                                                  text='Has max?')
        self.__has_max_checkbox = ttk.Checkbutton(
            self.__left_frame,
            state=tk.DISABLED,
            variable=self.__has_max_checkbox_var)
        # Max spinbox
        self.__max_spinbox_var = tk.IntVar(value='')
        self.__max_spinbox_var.trace('w', self.__on_max_changed)
        self.__max_spinbox = ttk.Spinbox(self.__left_frame,
                                         from_=0,
                                         to=math.inf,
                                         state=tk.DISABLED,
                                         textvariable=self.__max_spinbox_var,
                                         font=FONT)

    def _setup_layout(self):
        self.__taxonomy_tree.grid(row=0, column=1, sticky=tk.NSEW)
        self.__left_frame.grid(row=0,
                               column=0,
                               sticky=tk.NSEW,
                               pady=FRAME_PAD_Y,
                               padx=FRAME_PAD_X)
        self.__cmp_label.grid(row=0,
                              column=0,
                              columnspan=4,
                              sticky=tk.EW,
                              pady=CONTROL_PAD_Y)
        self.__has_association_checkbox_label.grid(row=1,
                                                   column=0,
                                                   columnspan=2,
                                                   sticky=tk.W,
                                                   pady=CONTROL_PAD_Y)
        self.__has_association_checkbox.grid(row=1,
                                             column=2,
                                             columnspan=2,
                                             sticky=tk.E,
                                             pady=CONTROL_PAD_Y)
        self.__has_min_checkbox_label.grid(row=2,
                                           column=0,
                                           sticky=tk.W,
                                           pady=CONTROL_PAD_Y)
        self.__has_min_checkbox.grid(row=2,
                                     column=1,
                                     sticky=tk.E,
                                     pady=CONTROL_PAD_Y)
        self.__has_max_checkbox_label.grid(row=2,
                                           column=2,
                                           sticky=tk.W,
                                           pady=CONTROL_PAD_Y)
        self.__has_max_checkbox.grid(row=2,
                                     column=3,
                                     sticky=tk.E,
                                     pady=CONTROL_PAD_Y)
        self.__min_spinbox.grid(row=3,
                                column=0,
                                columnspan=2,
                                sticky=tk.NSEW,
                                pady=CONTROL_PAD_Y)
        self.__max_spinbox.grid(row=3,
                                column=2,
                                columnspan=2,
                                sticky=tk.NSEW,
                                pady=CONTROL_PAD_Y)

        self.__left_frame.columnconfigure(0, weight=1)
        self.__left_frame.columnconfigure(1, weight=1)
        self.__left_frame.columnconfigure(2, weight=1)
        self.__left_frame.columnconfigure(3, weight=1)

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1, uniform='fred')
        self.columnconfigure(1, weight=3, uniform='fred')

    # SubscribesToListeners
    def _subscribe_to_events(self):
        pub.subscribe(self.__on_taxonomy_edited, actions.TAXONOMY_EDITED)
        pub.subscribe(self.__on_taxonomy_edited, actions.MODEL_LOADED)
        pub.subscribe(self._reset, actions.RESET)

    # Taxonomy Treeview
    def __on_select_tree_item(self, cmp_id: int) -> None:
        """Executed whenever a tree item is selected (by mouse click).

        :param cmp_id: Id of the selected component.
        """
        selected_cmp = self.__state.model.get_component(id_=cmp_id)
        if selected_cmp:
            change_controls_state(tk.NORMAL, self.__has_association_checkbox)
            self.__selected_component = selected_cmp
            if selected_cmp.association:
                self.__has_association_checkbox_var.set(True)
                change_controls_state(tk.NORMAL, self.__has_min_checkbox,
                                      self.__has_max_checkbox)
                if selected_cmp.association.min_:
                    self.__has_min_checkbox_var.set(True)
                    self.__min_spinbox_var.set(selected_cmp.association.min_)
                    change_controls_state(tk.NORMAL, self.__min_spinbox)
                else:
                    self.__has_min_checkbox_var.set(False)
                    change_controls_state(tk.DISABLED, self.__min_spinbox)
                    self.__min_spinbox_var.set('')
                if selected_cmp.association.max_:
                    self.__has_max_checkbox_var.set(True)
                    self.__max_spinbox_var.set(selected_cmp.association.max_)
                    change_controls_state(tk.NORMAL, self.__max_spinbox)
                else:
                    self.__has_max_checkbox_var.set(False)
                    change_controls_state(tk.DISABLED, self.__max_spinbox)
                    self.__max_spinbox_var.set('')
            else:
                self.__has_association_checkbox_var.set(False)
                self.__has_min_checkbox_var.set(
                    False)  # Reset and block controls
                self.__has_max_checkbox_var.set(False)
                self.__min_spinbox_var.set('')
                self.__max_spinbox_var.set('')
                change_controls_state(tk.DISABLED, self.__has_max_checkbox,
                                      self.__has_min_checkbox,
                                      self.__max_spinbox, self.__min_spinbox)
            self.__cmp_label_var.set(trim_string(selected_cmp.name, length=20))

    @staticmethod
    def __extract_values(cmp: Component) -> Tuple[Any, ...]:
        """Extracts the data of the component to show in the taxonomy view.

        :param cmp: Component from which to extract the data.
        :return: Tuple containing data about component
            (has association?, min_, max_?).
        """
        has_association = ''
        min_ = ''
        max_ = ''
        if cmp.association:
            has_association = 'Yes'
            min_ = cmp.association.min_ if cmp.association.min_ is not None else ''
            max_ = cmp.association.max_ if cmp.association.max_ is not None else ''
        return has_association, min_, max_

    def __build_tree(self) -> None:
        """Fills the tree view with components from model."""
        self.__taxonomy_tree.set_items(self.__state.model.taxonomy)

    # Resetable
    def _reset(self) -> None:
        self.__taxonomy_tree.set_items([])
        self.__selected_component = None
        # Set entries to default
        self.__has_association_checkbox_var.set(False)
        self.__has_min_checkbox_var.set(False)
        self.__has_max_checkbox_var.set(False)
        self.__min_spinbox_var.set('')
        self.__max_spinbox_var.set('')
        self.__cmp_label_var.set('')
        # Disable items
        change_controls_state(tk.DISABLED, self.__has_association_checkbox,
                              self.__has_min_checkbox, self.__has_max_checkbox,
                              self.__min_spinbox, self.__max_spinbox)

    def __on_taxonomy_edited(self) -> None:
        """Executed whenever the structure of the taxonomy changes."""
        self._reset()
        self.__build_tree()

    def __on_has_association_changed(self, *_):
        """Executed whenever the __has_association_checkbox is toggled"""
        if self.__selected_component:
            has_association = self.__has_association_checkbox_var.get()
            if has_association:
                if not self.__selected_component.association:
                    self.__selected_component.association = Association()
                change_controls_state(tk.NORMAL, self.__has_min_checkbox,
                                      self.__has_max_checkbox)
            else:
                self.__has_min_checkbox_var.set(
                    False)  # Reset and block controls
                self.__has_max_checkbox_var.set(False)
                self.__min_spinbox_var.set('')
                self.__max_spinbox_var.set('')
                change_controls_state(tk.DISABLED, self.__has_max_checkbox,
                                      self.__has_min_checkbox,
                                      self.__max_spinbox, self.__min_spinbox)
                self.__selected_component.association = None
            self.__taxonomy_tree.update_values(self.__selected_component)

    def __on_has_min_changed(self, *_):
        """Executed whenever the __has_min_checkbox is toggled"""
        if self.__selected_component:
            has_min = self.__has_min_checkbox_var.get()
            if has_min:
                change_controls_state(tk.NORMAL, self.__min_spinbox)
                if self.__selected_component.association and self.__selected_component.association.min_ is not None:
                    self.__min_spinbox_var.set(
                        self.__selected_component.association.min_)
                else:
                    self.__min_spinbox_var.set(0)
                    self.__selected_component.association.min_ = 0
            else:
                if self.__selected_component.association:
                    self.__selected_component.association.min_ = None
                change_controls_state(tk.DISABLED, self.__min_spinbox)
                self.__min_spinbox_var.set('')

    def __on_has_max_changed(self, *_):
        """Executed whenever the __has_max_checkbox_var is toggled"""
        if self.__selected_component:
            has_max = self.__has_max_checkbox_var.get()
            if has_max:
                change_controls_state(tk.NORMAL, self.__max_spinbox)
                if self.__selected_component.association:
                    if self.__selected_component.association.max_ is not None:
                        self.__max_spinbox_var.set(
                            self.__selected_component.association.max_)
                    else:
                        if self.__selected_component.association.min_ is not None:
                            # If component has a minimum association, set the value of Spinbox to it
                            self.__max_spinbox_var.set(
                                self.__selected_component.association.min_)
                        else:
                            # Otherwise set it to 0
                            self.__max_spinbox_var.set(0)
                            self.__selected_component.association.max_ = 0
            else:
                if self.__selected_component.association:
                    self.__selected_component.association.max_ = None
                change_controls_state(tk.DISABLED, self.__max_spinbox)
                self.__max_spinbox_var.set('')

    def __on_min_changed(self, *_):
        """Executed whenever the __min_spinbox_var value changes."""
        if self.__selected_component and self.__selected_component.association:
            # This gets triggered at unpredicted moments (e.g. enabling and disabling widgets
            # so it's necessary to check this condition.
            try:
                min_ = self.__min_spinbox_var.get()
                self.__selected_component.association.min_ = min_
                if self.__selected_component.association.max_ is not None \
                   and self.__selected_component.association.max_ < min_:  # If max < min; set max to min
                    self.__selected_component.association.max_ = min_
                    self.__max_spinbox_var.set(min_)
            except tk.TclError:
                self.__selected_component.association.min_ = None
            finally:
                self.__taxonomy_tree.update_values(self.__selected_component)

    def __on_max_changed(self, *_):
        """Executed whenever the __max_spinbox_var value changes."""
        if self.__selected_component and self.__selected_component.association:
            try:
                max_ = self.__max_spinbox_var.get()
                self.__selected_component.association.max_ = max_
                if self.__selected_component.association.min_ is not None \
                   and self.__selected_component.association.min_ > max_:  # If min > max; set min to max
                    self.__selected_component.association.min_ = max_
                    self.__min_spinbox_var.set(max_)
            except tk.TclError:
                self.__selected_component.association.max_ = None
            finally:
                self.__taxonomy_tree.update_values(self.__selected_component)