コード例 #1
0
    def __init__(self, project, parent_widget = None):
        QtGui.QDialog.__init__(self, parent_widget)
        self.setupUi(self)


        self.project = project
        self.submodel_node = None # the submodel that we are editing (a copy of actual submodel node)
        self.active_variables_node = None
        self.selector_table_model = VariableSelectorTableModel(project)

        self.tree_structure_editor.header().setStretchLastSection(True)
        self.tree_structure_editor.header().setMinimumWidth(50)

        self.frame_name_warning.setVisible(False)
        self.pb_remove_variable.setVisible(False)
        # hide the name warning when the user edit the name
        hide_widget_on_value_change(self.lbl_name_warning, self.le_name)

        S = QtCore.SIGNAL # temporarily use a shorter name for all the connections below

        self.connect(self.selector_table_model, S('layoutChanged()'), self._selector_model_column_resize)
        signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)")
        self.connect(self.tree_structure_selector, signal, self._change_structure_node)
        signal = S('currentIndexChanged(int)')
        self.connect(self.cbo_dataset_filter, signal, self._update_available_variables)

        # Setup Variable Selector Table
        self.table_selected_variables.setModel(self.selector_table_model)
        self.table_selected_variables.horizontalHeader().setStretchLastSection(True)
        self.table_selected_variables.verticalHeader().hide()
        self.table_selected_variables.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        signal = S("customContextMenuRequested(const QPoint &)")
        self.connect(self.table_selected_variables, signal, self._right_click_variables)

        f_create_nest = lambda x = 'nest': self.tree_structure_editor.create_structure_node(x)
        f_create_equation = lambda x = 'equation': self.tree_structure_editor.create_structure_node(x)
        self.connect(self.pb_create_nest, S('released()'), f_create_nest)
        self.connect(self.pb_create_equation, S('released()'), f_create_equation)

        self.connect(self.buttonBox, S('rejected()'), self.reject)
        self.connect(self.buttonBox, S('accepted()'), self.validate_submodel_and_accept)
        # the label "OK" can be confusing when switching between the structure
        # editor and the variable selector. Some users clicked "OK" to confirm the structure changes
        # Therefore we set a more explicit label.
        self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close')

        signal = S('structure_changed')
        self.connect(self.tree_structure_editor, signal, self._update_submodel_structure_trees)
        signal = S('clicked()')
        self.connect(self.pb_update_model_structure, signal, self.update_model_nested_structure)
コード例 #2
0
    def __init__(self, project, parent_widget=None):
        QtGui.QDialog.__init__(self, parent_widget)
        self.setupUi(self)

        self.project = project
        self.submodel_node = None  # the submodel that we are editing (a copy of actual submodel node)
        self.active_variables_node = None
        self.selector_table_model = VariableSelectorTableModel(project)

        self.tree_structure_editor.header().setStretchLastSection(True)
        self.tree_structure_editor.header().setMinimumWidth(50)

        self.frame_name_warning.setVisible(False)
        self.pb_remove_variable.setVisible(False)
        # hide the name warning when the user edit the name
        hide_widget_on_value_change(self.lbl_name_warning, self.le_name)

        S = QtCore.SIGNAL  # temporarily use a shorter name for all the connections below

        self.connect(self.selector_table_model, S('layoutChanged()'),
                     self._selector_model_column_resize)
        signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)")
        self.connect(self.tree_structure_selector, signal,
                     self._change_structure_node)
        signal = S('currentIndexChanged(int)')
        self.connect(self.cbo_dataset_filter, signal,
                     self._update_available_variables)

        # Setup Variable Selector Table
        self.table_selected_variables.setModel(self.selector_table_model)
        self.table_selected_variables.horizontalHeader().setStretchLastSection(
            True)
        self.table_selected_variables.verticalHeader().hide()
        self.table_selected_variables.setContextMenuPolicy(
            QtCore.Qt.CustomContextMenu)
        signal = S("customContextMenuRequested(const QPoint &)")
        self.connect(self.table_selected_variables, signal,
                     self._right_click_variables)

        f_create_nest = lambda x='nest': self.tree_structure_editor.create_structure_node(
            x)
        f_create_equation = lambda x='equation': self.tree_structure_editor.create_structure_node(
            x)
        self.connect(self.pb_create_nest, S('released()'), f_create_nest)
        self.connect(self.pb_create_equation, S('released()'),
                     f_create_equation)

        self.connect(self.buttonBox, S('rejected()'), self.reject)
        self.connect(self.buttonBox, S('accepted()'),
                     self.validate_submodel_and_accept)
        # the label "OK" can be confusing when switching between the structure
        # editor and the variable selector. Some users clicked "OK" to confirm the structure changes
        # Therefore we set a more explicit label.
        self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close')

        signal = S('structure_changed')
        self.connect(self.tree_structure_editor, signal,
                     self._update_submodel_structure_trees)
        signal = S('clicked()')
        self.connect(self.pb_update_model_structure, signal,
                     self.update_model_nested_structure)
コード例 #3
0
class SubModelEditor(QtGui.QDialog, Ui_SubModelEditor):
    '''
    Submodel Editing dialog.
    The editor support three different structures of submodels:
    I call these structures Plain Structures, Equation Structures and Nested Structures
    The Plain Structures are submodels that only have a variable list.
    The Equation Structures are submodels that have one or more <equation>, with each equation
    having it's own variable list.
    The Nested Structures have one or more levels of <nest>:s. Each nest can either have an
    <equation> (that in turn have a variable list) or another nest.
    Nests can not have variable lists themselves.

    The assignment of variables happen on either the different <equation>:s (in the case of
    Equation Structures and Nested Structures) or on the submodel itself if it has a Plain Structure

    The GUI dialog is made somewhat simpler if the submodel has a Plain Structure, as some
    functionality is not needed in this case.
    '''
    def __init__(self, project, parent_widget=None):
        QtGui.QDialog.__init__(self, parent_widget)
        self.setupUi(self)

        self.project = project
        self.submodel_node = None  # the submodel that we are editing (a copy of actual submodel node)
        self.active_variables_node = None
        self.selector_table_model = VariableSelectorTableModel(project)

        self.tree_structure_editor.header().setStretchLastSection(True)
        self.tree_structure_editor.header().setMinimumWidth(50)

        self.frame_name_warning.setVisible(False)
        self.pb_remove_variable.setVisible(False)
        # hide the name warning when the user edit the name
        hide_widget_on_value_change(self.lbl_name_warning, self.le_name)

        S = QtCore.SIGNAL  # temporarily use a shorter name for all the connections below

        self.connect(self.selector_table_model, S('layoutChanged()'),
                     self._selector_model_column_resize)
        signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)")
        self.connect(self.tree_structure_selector, signal,
                     self._change_structure_node)
        signal = S('currentIndexChanged(int)')
        self.connect(self.cbo_dataset_filter, signal,
                     self._update_available_variables)

        # Setup Variable Selector Table
        self.table_selected_variables.setModel(self.selector_table_model)
        self.table_selected_variables.horizontalHeader().setStretchLastSection(
            True)
        self.table_selected_variables.verticalHeader().hide()
        self.table_selected_variables.setContextMenuPolicy(
            QtCore.Qt.CustomContextMenu)
        signal = S("customContextMenuRequested(const QPoint &)")
        self.connect(self.table_selected_variables, signal,
                     self._right_click_variables)

        f_create_nest = lambda x='nest': self.tree_structure_editor.create_structure_node(
            x)
        f_create_equation = lambda x='equation': self.tree_structure_editor.create_structure_node(
            x)
        self.connect(self.pb_create_nest, S('released()'), f_create_nest)
        self.connect(self.pb_create_equation, S('released()'),
                     f_create_equation)

        self.connect(self.buttonBox, S('rejected()'), self.reject)
        self.connect(self.buttonBox, S('accepted()'),
                     self.validate_submodel_and_accept)
        # the label "OK" can be confusing when switching between the structure
        # editor and the variable selector. Some users clicked "OK" to confirm the structure changes
        # Therefore we set a more explicit label.
        self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close')

        signal = S('structure_changed')
        self.connect(self.tree_structure_editor, signal,
                     self._update_submodel_structure_trees)
        signal = S('clicked()')
        self.connect(self.pb_update_model_structure, signal,
                     self.update_model_nested_structure)

    def _lookup_model_node_for(self, node):
        ''' seek up the tree structure for the <model> parent of the submodel node '''
        while node is not None:
            if node.tag == 'model':
                return node
            node = node.getparent()
        return None

    def _change_structure_node(self, new_item, old_item):
        self._set_variable_list_node(
            new_item.variable_list() if new_item else None)

    def _show_name_warning(self, text):
        self.lbl_name_warning.setText(text)
        self.frame_name_warning.setVisible(True)
        self.le_name.selectAll()
        self.le_name.setFocus()

    def _set_variable_list_node(self, variable_list_node):
        ''' populate the list of selected variables with the variable_spec nodes of the given
        variable_list_node '''
        # "save' the changes to the previously edited variable_list before changing active node
        self._apply_selected_variables(self.active_variables_node)
        self.active_variables_node = variable_list_node

        self.selector_table_model.clear()

        if variable_list_node is not None:
            for variable_spec_node in variable_list_node:
                self.selector_table_model.add_variable_spec_node(
                    variable_spec_node)
            self.table_selected_variables.setEnabled(True)
            self._selector_model_column_resize()
            self.pb_show_picker.setEnabled(True)
        else:
            self.table_selected_variables.setEnabled(False)
            self.pb_show_picker.setEnabled(False)

    def _apply_selected_variables(self, variables_node):
        if variables_node is None:
            return
        self.selector_table_model.apply_selected_variables(variables_node)

    def _update_submodel_structure_trees(self):
        ''' updates both of the tree widgets to show the structure of self.submodel_node '''
        self.tree_structure_selector.clear()
        self.tree_structure_editor.clear()
        self._populate_structure_tree(self.submodel_node, False,
                                      self.tree_structure_selector)
        self._populate_structure_tree(self.submodel_node, True,
                                      self.tree_structure_editor)

        for tree_widget in [
                self.tree_structure_editor, self.tree_structure_selector
        ]:
            tree_widget.resizeColumnToContents(0)
            tree_widget.resizeColumnToContents(1)

        # make the GUI a little simpler if the submodel is "plain" (i.e has no structural elements)
        # by automatically hiding the "structure selector" tree
        if self._in_simple_mode():
            self.split_struct_variables.setSizes([0, 10
                                                  ])  # hide structure selector
            # auto select the only variable_list
            self._set_variable_list_node(
                self.submodel_node.find('variable_list'))
        else:
            # make sure that the structure widget is visible
            if not self.pb_show_picker.isChecked():
                self.stack_struct_picker.setCurrentIndex(1)
            self.split_struct_variables.setSizes([10, 10])
            self._set_variable_list_node(None)
            # auto select the first structural element
            item = self.tree_structure_selector.topLevelItem(0)
            self.tree_structure_selector.setCurrentItem(item)

    def _populate_structure_tree(self, parent_node, editable, parent_widget):
        ''' adds all <nest> nodes and <equation> nodes of parent_node to parent_widget. Recurses
        down added <nest> nodes. '''
        nest_nodes = parent_node.findall('nest')
        equation_nodes = parent_node.findall('equation')
        # recursively add nest nodes
        for nest_node in nest_nodes:
            item = SubmodelStructureItem(nest_node, editable, parent_widget)
            item.setExpanded(True)
            self._populate_structure_tree(nest_node, editable, item)
        # add any equations
        for equation_node in equation_nodes:
            item = SubmodelStructureItem(equation_node, editable,
                                         parent_widget)

    def _selector_model_column_resize(self):
        ''' updates the column widths whenever there has been a layout change to the model '''
        self.selector_table_model.sort_variables_by_name()
        self.table_selected_variables.resizeRowsToContents()
        for col in [0, 3, 4]:
            self.table_selected_variables.resizeColumnToContents(col)

    def _validate_names(self, show_error_message=False):
        ''' go through all nest and equation names and ensure that there are no collisions.
        Returns True if all the names are valid and False otherwise.
        If @param show_error_message is True an error message of the name errors is displayed.'''
        # Check for colliding names among the nest and equations
        colliding_names = set()
        nodes_to_inspect = self.submodel_node.findall('.//nest')
        nodes_to_inspect.extend(self.submodel_node.findall('.//equation'))
        for inspected_node in nodes_to_inspect:
            # get all sibling names with the same tag
            sibling_names = [
                node.get('name') for node in inspected_node.getparent() if
                node is not inspected_node and node.tag == inspected_node.tag
            ]
            # if there is a name collision, add the name to the set of found colliding names
            if inspected_node.get('name') in sibling_names:
                parent_node = inspected_node.getparent()
                if parent_node.tag == 'nest':
                    desc = '&lt;%s&gt;/%s' % (parent_node.get('name'),
                                              inspected_node.get('name'))
                else:
                    desc = '%s' % inspected_node.get('name')
                colliding_names.add(desc)

        # the concept of colliding names might be confusing so we want to be clear on what is
        # happening and (more importantly) how to solve it
        if colliding_names:
            if not show_error_message:
                return False
            str_collide_list = ''.join(
                ['<li>%s</li>\n' % name for name in colliding_names])
            short_msg = 'Name collisions found.'
            longer_msg = ''' <qt> Colliding names:
            <b> <ul> %s </ul> </b>
            <p>A name collision is when there are two items with the same name, the same type and the same level.</p>
            For example:
            <ul>
                <li>MY_NEST</li>
                <li><ul><li>MY_EQUATION</li><li>MY_EQUATION</li></ul></li>
            </ul>
            <p>will cause a name collision, while this example;</p>
            <ul>
                <li>MY_NEST</li>
                <li><ul><li>MY_EQUATION</li></ul></li>
                <li>MY_OTHER_NEST</li> <li>
                <ul><li>MY_EQUATION</li></ul></li>
            </ul>
            <p>is fine since the two equations with the same name are on different levels.</p>
            <p>To correct this error please give unique names for the above mentioned equations
            and/or nests.</p></qt>''' % str_collide_list
            MessageBox.warning(self, short_msg, longer_msg)
            return False
        return True

    def _create_nested_structure_xml(self, nest_node, counter):
        ''' create and return an XML representation of the nested_structure for the given node '''
        # quickie method to get the nest_id or equiation_id from a node dep. on it's tag
        the_id = lambda x: str(x.get('nest_id')) if x.tag == 'nest' else str(
            x.get('equation_id'))

        # the submodel node is passed in the first time this method is called, so we create the
        # special containing node then
        if nest_node.tag == 'submodel':
            created_nest_node = etree.Element('argument', {
                'name': 'nested_structure',
                'type': 'dictionary'
            })
        else:
            attrib = {
                'name': the_id(nest_node),
                'type': 'dictionary',
                'parser_action': 'convert_key_to_integer'
            }
            created_nest_node = etree.Element('nest', attrib)

        if nest_node.find(
                'nest') is not None:  # decend into multiple levels of nests
            for nest_child_node in nest_node.findall('nest'):
                child_xml = self._create_nested_structure_xml(
                    nest_child_node, counter)
                created_nest_node.append(child_xml)
        elif nest_node.get('number_of_samples') is not None:
            # auto generate a series of equation ID's
            num_of_samples = int(nest_node.get('number_of_samples'))
            id_start = counter['current count']
            counter['current count'] = id_end = id_start + num_of_samples
            index_range = range(id_start, id_end)
            created_nest_node.set('type', 'list')
            created_nest_node.text = repr(index_range)
        elif nest_node.find('equation') is not None:
            index_range = [
                int(the_id(x)) for x in nest_node.findall('equation')
            ]
            created_nest_node.set('type', 'list')
            created_nest_node.text = repr(index_range)
        else:
            raise ValueError(
                'Found empty nest without "number_of_samples" attribute or <equation> '
                'child nodes (%s name=%s)' %
                (nest_node.tag, nest_node.get('name')))
        return created_nest_node

    def _create_add_variable_menu(self):
        # function to display variables in the popup menu
        def display_node(node, selected_nodes):
            if node in selected_nodes:
                return '(already selected) %s' % get_variable_name(node)
            return get_variable_name(node)

        # call back to only add unselected variables from the popup menu
        def add_if_unselected(node, selected_nodes):
            if not node in selected_nodes:
                self.add_variable(node)

        dataset_variable_nodes = get_variable_nodes_per_dataset(self.project)
        selected_names = map(get_variable_name,
                             self.selector_table_model._variable_nodes)
        selected_nodes = []

        for variable_node_list in dataset_variable_nodes.values():
            for variable_node in variable_node_list:
                if get_variable_name(variable_node) in selected_names:
                    selected_nodes.append(variable_node)
            # sort by variable name
            variable_node_list.sort(
                lambda x, y: cmp(get_variable_name(x), get_variable_name(y)))
            # display selected items at the bottom
            for node in variable_node_list[:]:
                if node in selected_nodes:
                    variable_node_list.remove(node)
                    variable_node_list.insert(len(variable_node_list), node)

        display_func = lambda x, y=selected_nodes: display_node(x, y)
        callback = lambda x, y=selected_nodes: add_if_unselected(x, y)
        return dictionary_to_menu(source_dict=dataset_variable_nodes,
                                  callback=callback,
                                  display_func=display_func,
                                  parent_widget=self)

    def _right_click_variables(self, point):
        ''' construct and show an operations operations_menu when the user right clicks the variable selector '''
        operations_menu = QtGui.QMenu(self)
        add_variable_menu = self._create_add_variable_menu()
        add_variable_menu.setTitle('Add a variable')
        add_variable_menu.setIcon(IconLibrary.icon('add'))
        operations_menu.addMenu(add_variable_menu)

        index_under_cursor = self.table_selected_variables.indexAt(point)
        if index_under_cursor.isValid():
            row = index_under_cursor.row()
            variable_node = self.selector_table_model.get_variable(row)
            self.table_selected_variables.selectRow(row)
            action = create_qt_action(
                None, 'Remove %s' % get_variable_name(variable_node),
                self._remove_selected_variables, self.table_selected_variables)
            action.setIcon(IconLibrary.icon('delete'))
            operations_menu.addAction(action)
        else:
            # if the index is not valid -- assume that the user clicked the "white space" of the table
            pass
        operations_menu.exec_(QtGui.QCursor.pos())

    def _in_simple_mode(self):
        return self.tree_structure_selector.topLevelItemCount() == 0

    def _update_available_variables(self):
        # populate the list of available variables while considering A) what dataset filter the user
        # has selected, an d B) what variables that have already been selected)
        self.lst_available_variables.clear()
        if self.cbo_dataset_filter.currentIndex() > 0:
            dataset_filter = str(self.cbo_dataset_filter.currentText())
        else:
            dataset_filter = None
        selected_variable_names = [
            node.get('name')
            for node in self.selector_table_model._variable_nodes
        ]

        available_variable_nodes = []
        variable_nodes_per_dataset = get_variable_nodes_per_dataset(
            self.project)

        if not dataset_filter:  # take all variables
            for variable_nodes in variable_nodes_per_dataset.values():
                available_variable_nodes.extend(variable_nodes)
        else:
            available_variable_nodes = variable_nodes_per_dataset[
                dataset_filter]
        available_variable_nodes.append(get_built_in_constant_node())

        # filter already selected variables and show the list of available variables
        not_selected_variables = [
            var_node for var_node in available_variable_nodes
            if not var_node.get('name') in selected_variable_names
        ]
        for variable_node in not_selected_variables:
            item = QtGui.QListWidgetItem(self.lst_available_variables)
            item.setIcon(IconLibrary.icon('variable'))
            item.node = variable_node  # monkey in the node for adding later
            item.setText(get_variable_name(variable_node))
            self.lst_available_variables.addItem(item)
        self.lst_available_variables.sortItems()

    def _update_dataset_filter_list(self):
        # update the combo box list (keeping selection)
        variable_nodes_per_dataset = get_variable_nodes_per_dataset(
            self.project)
        pre_update_dataset_name = self.cbo_dataset_filter.currentText()
        self.cbo_dataset_filter.clear()
        self.cbo_dataset_filter.addItem('[All datasets]')
        for dataset_name in variable_nodes_per_dataset:
            if dataset_name is not None:  # built ins are manually added
                self.cbo_dataset_filter.addItem(dataset_name)
        post_update_index = self.cbo_dataset_filter.findText(
            pre_update_dataset_name)
        if post_update_index > -1:
            self.cbo_dataset_filter.setCurrentIndex(post_update_index)

    def _remove_selected_variables(self):
        # delete the rows from highest to lowest so we don't change the row number when we delete
        # a row (i.e if we want to delete row 1 and 2 and start with one, then the row that was
        # number 2 will have become one and we will actually delete row number 3
        selected_indices = self.table_selected_variables.selectedIndexes()
        selected_rows = list(set([idx.row() for idx in selected_indices]))
        selected_rows.sort(reverse=True)
        for row in selected_rows:
            self.table_selected_variables.model().removeRow(row)
        self._update_available_variables()

    def _show_advanced_parameters(self, set_advanced_visible=None):
        # updates the selector table to show only the basic data if set_advanced_visible is True
        # otherwise all available data is shown
        if set_advanced_visible is None:
            set_advanced_visible = self.cb_show_advanced_parameters.isChecked()
        if set_advanced_visible:
            for column in range(5):
                self.table_selected_variables.showColumn(column)
        else:
            advanced_columns = [0, 2, 3, 4]
            for column in range(5):
                if column in advanced_columns:
                    self.table_selected_variables.hideColumn(column)
                else:
                    self.table_selected_variables.showColumn(column)

    def _set_picker_visible(self, visible):
        # shows / hides the variable picker
        # visible =
        if visible:
            self.stack_struct_picker.setCurrentIndex(0)
            if self._in_simple_mode():
                self.split_struct_variables.setSizes([1, 1])
            # hide unnecessary information while picking variables
            self._show_advanced_parameters(False)
        else:
            self.stack_struct_picker.setCurrentIndex(1)
            if self._in_simple_mode():
                self.split_struct_variables.setSizes([0, 10])
            self._show_advanced_parameters()
            # sometimes the table gets messed up when shown again so force refresh
            self.selector_table_model.emit(QtCore.SIGNAL("layoutChanged()"))
        self.cb_show_advanced_parameters.setEnabled(not visible)
        self.pb_remove_variable.setVisible(visible)
        self.pb_show_picker.setChecked(visible)

    def update_model_nested_structure(self, node=None):
        '''
        Create an XML representation to use for the argument "nested_structure" used by Nested Logit
        Models (NLM). The argument is used in the NLMs init() method.
        @param node submodel node to construct a nested structure from (default self.submodel_node)
        @raise RuntimeError: If the model node could not be updated
        @return the created nested_structure node (useful for tests)
        '''
        node = self.submodel_node if node is None else node
        if node.find(
                'nest'
        ) is None:  # can't create a nested structure if there are no nests
            return None
        counter = {
            'current count': 1
        }  # pass object ref to keep increments down the recursive chain
        try:
            new_nested_structure_node = self._create_nested_structure_xml(
                node, counter)
        except ValueError, ex:
            MessageBox.error(self,
                             'Not all nests have equations assigned to them.',
                             str(ex))
            return None
        model_node = self._lookup_model_node_for(node)
        if new_nested_structure_node is None or model_node is None:
            err_msg = 'Warning: Could not update the model nested node because:'
            if new_nested_structure_node is None:
                err_msg = err_msg + ' * the created "nested_structure" was empty'
            if model_node is None:
                err_msg = err_msg + ' * a parent <model> node to update could not be found'
            raise RuntimeError(err_msg)

        # replace existing nested_structure(s) with the new one
        init_node = model_node.find('structure/init/')
        for existing_nest_node in init_node.findall(
                "argument[@name='nested_structure']"):
            init_node.remove(existing_nest_node)
        init_node.append(new_nested_structure_node)
        return new_nested_structure_node
コード例 #4
0
class SubModelEditor(QtGui.QDialog, Ui_SubModelEditor):

    '''
    Submodel Editing dialog.
    The editor support three different structures of submodels:
    I call these structures Plain Structures, Equation Structures and Nested Structures
    The Plain Structures are submodels that only have a variable list.
    The Equation Structures are submodels that have one or more <equation>, with each equation
    having it's own variable list.
    The Nested Structures have one or more levels of <nest>:s. Each nest can either have an
    <equation> (that in turn have a variable list) or another nest.
    Nests can not have variable lists themselves.

    The assignment of variables happen on either the different <equation>:s (in the case of
    Equation Structures and Nested Structures) or on the submodel itself if it has a Plain Structure

    The GUI dialog is made somewhat simpler if the submodel has a Plain Structure, as some
    functionality is not needed in this case.
    '''

    def __init__(self, project, parent_widget = None):
        QtGui.QDialog.__init__(self, parent_widget)
        self.setupUi(self)


        self.project = project
        self.submodel_node = None # the submodel that we are editing (a copy of actual submodel node)
        self.active_variables_node = None
        self.selector_table_model = VariableSelectorTableModel(project)

        self.tree_structure_editor.header().setStretchLastSection(True)
        self.tree_structure_editor.header().setMinimumWidth(50)

        self.frame_name_warning.setVisible(False)
        self.pb_remove_variable.setVisible(False)
        # hide the name warning when the user edit the name
        hide_widget_on_value_change(self.lbl_name_warning, self.le_name)

        S = QtCore.SIGNAL # temporarily use a shorter name for all the connections below

        self.connect(self.selector_table_model, S('layoutChanged()'), self._selector_model_column_resize)
        signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)")
        self.connect(self.tree_structure_selector, signal, self._change_structure_node)
        signal = S('currentIndexChanged(int)')
        self.connect(self.cbo_dataset_filter, signal, self._update_available_variables)

        # Setup Variable Selector Table
        self.table_selected_variables.setModel(self.selector_table_model)
        self.table_selected_variables.horizontalHeader().setStretchLastSection(True)
        self.table_selected_variables.verticalHeader().hide()
        self.table_selected_variables.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        signal = S("customContextMenuRequested(const QPoint &)")
        self.connect(self.table_selected_variables, signal, self._right_click_variables)

        f_create_nest = lambda x = 'nest': self.tree_structure_editor.create_structure_node(x)
        f_create_equation = lambda x = 'equation': self.tree_structure_editor.create_structure_node(x)
        self.connect(self.pb_create_nest, S('released()'), f_create_nest)
        self.connect(self.pb_create_equation, S('released()'), f_create_equation)

        self.connect(self.buttonBox, S('rejected()'), self.reject)
        self.connect(self.buttonBox, S('accepted()'), self.validate_submodel_and_accept)
        # the label "OK" can be confusing when switching between the structure
        # editor and the variable selector. Some users clicked "OK" to confirm the structure changes
        # Therefore we set a more explicit label.
        self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close')

        signal = S('structure_changed')
        self.connect(self.tree_structure_editor, signal, self._update_submodel_structure_trees)
        signal = S('clicked()')
        self.connect(self.pb_update_model_structure, signal, self.update_model_nested_structure)

    def _lookup_model_node_for(self, node):
        ''' seek up the tree structure for the <model> parent of the submodel node '''
        while node is not None:
            if node.tag == 'model':
                return node
            node = node.getparent()
        return None

    def _change_structure_node(self, new_item, old_item):
        self._set_variable_list_node(new_item.variable_list() if new_item else None)

    def _show_name_warning(self, text):
        self.lbl_name_warning.setText(text)
        self.frame_name_warning.setVisible(True)
        self.le_name.selectAll()
        self.le_name.setFocus()

    def _set_variable_list_node(self, variable_list_node):
        ''' populate the list of selected variables with the variable_spec nodes of the given
        variable_list_node '''
        # "save' the changes to the previously edited variable_list before changing active node
        self._apply_selected_variables(self.active_variables_node)
        self.active_variables_node = variable_list_node

        self.selector_table_model.clear()

        if variable_list_node is not None:
            for variable_spec_node in variable_list_node:
                self.selector_table_model.add_variable_spec_node(variable_spec_node)
            self.table_selected_variables.setEnabled(True)
            self._selector_model_column_resize()
            self.pb_show_picker.setEnabled(True)
        else:
            self.table_selected_variables.setEnabled(False)
            self.pb_show_picker.setEnabled(False)

    def _apply_selected_variables(self, variables_node):
        if variables_node is None:
            return
        self.selector_table_model.apply_selected_variables(variables_node)

    def _update_submodel_structure_trees(self):
        ''' updates both of the tree widgets to show the structure of self.submodel_node '''
        self.tree_structure_selector.clear()
        self.tree_structure_editor.clear()
        self._populate_structure_tree(self.submodel_node, False, self.tree_structure_selector)
        self._populate_structure_tree(self.submodel_node, True, self.tree_structure_editor)

        for tree_widget in [self.tree_structure_editor, self.tree_structure_selector]:
            tree_widget.resizeColumnToContents(0)
            tree_widget.resizeColumnToContents(1)

        # make the GUI a little simpler if the submodel is "plain" (i.e has no structural elements)
        # by automatically hiding the "structure selector" tree
        if self._in_simple_mode():
            self.split_struct_variables.setSizes([0, 10]) # hide structure selector
            # auto select the only variable_list
            self._set_variable_list_node(self.submodel_node.find('variable_list'))
        else:
            # make sure that the structure widget is visible
            if not self.pb_show_picker.isChecked():
                self.stack_struct_picker.setCurrentIndex(1)
            self.split_struct_variables.setSizes([10, 10])
            self._set_variable_list_node(None)
            # auto select the first structural element
            item = self.tree_structure_selector.topLevelItem(0)
            self.tree_structure_selector.setCurrentItem(item)

    def _populate_structure_tree(self, parent_node, editable, parent_widget):
        ''' adds all <nest> nodes and <equation> nodes of parent_node to parent_widget. Recurses
        down added <nest> nodes. '''
        nest_nodes = parent_node.findall('nest')
        equation_nodes = parent_node.findall('equation')
        # recursively add nest nodes
        for nest_node in nest_nodes:
            item = SubmodelStructureItem(nest_node, editable, parent_widget)
            item.setExpanded(True)
            self._populate_structure_tree(nest_node, editable, item)
        # add any equations
        for equation_node in equation_nodes:
            item = SubmodelStructureItem(equation_node, editable, parent_widget)

    def _selector_model_column_resize(self):
        ''' updates the column widths whenever there has been a layout change to the model '''
        self.selector_table_model.sort_variables_by_name()
        self.table_selected_variables.resizeRowsToContents()
        for col in [0, 3, 4]:
            self.table_selected_variables.resizeColumnToContents(col)

    def _validate_names(self, show_error_message = False):
        ''' go through all nest and equation names and ensure that there are no collisions.
        Returns True if all the names are valid and False otherwise.
        If @param show_error_message is True an error message of the name errors is displayed.'''
        # Check for colliding names among the nest and equations
        colliding_names = set()
        nodes_to_inspect = self.submodel_node.findall('.//nest')
        nodes_to_inspect.extend(self.submodel_node.findall('.//equation'))
        for inspected_node in nodes_to_inspect:
            # get all sibling names with the same tag
            sibling_names = [node.get('name') for node in inspected_node.getparent() if
                             node is not inspected_node and node.tag == inspected_node.tag]
            # if there is a name collision, add the name to the set of found colliding names
            if inspected_node.get('name') in sibling_names:
                parent_node = inspected_node.getparent()
                if parent_node.tag == 'nest':
                    desc = '&lt;%s&gt;/%s' % (parent_node.get('name'), inspected_node.get('name'))
                else:
                    desc = '%s' % inspected_node.get('name')
                colliding_names.add(desc)

        # the concept of colliding names might be confusing so we want to be clear on what is
        # happening and (more importantly) how to solve it
        if colliding_names:
            if not show_error_message:
                return False
            str_collide_list = ''.join(['<li>%s</li>\n' % name for name in colliding_names])
            short_msg = 'Name collisions found.'
            longer_msg = ''' <qt> Colliding names:
            <b> <ul> %s </ul> </b>
            <p>A name collision is when there are two items with the same name, the same type and the same level.</p>
            For example:
            <ul>
                <li>MY_NEST</li>
                <li><ul><li>MY_EQUATION</li><li>MY_EQUATION</li></ul></li>
            </ul>
            <p>will cause a name collision, while this example;</p>
            <ul>
                <li>MY_NEST</li>
                <li><ul><li>MY_EQUATION</li></ul></li>
                <li>MY_OTHER_NEST</li> <li>
                <ul><li>MY_EQUATION</li></ul></li>
            </ul>
            <p>is fine since the two equations with the same name are on different levels.</p>
            <p>To correct this error please give unique names for the above mentioned equations
            and/or nests.</p></qt>'''% str_collide_list
            MessageBox.warning(self, short_msg, longer_msg)
            return False
        return True

    def _create_nested_structure_xml(self, nest_node, counter):
        ''' create and return an XML representation of the nested_structure for the given node '''
        # quickie method to get the nest_id or equiation_id from a node dep. on it's tag
        the_id = lambda x: str(x.get('nest_id')) if x.tag == 'nest' else str(x.get('equation_id'))

        # the submodel node is passed in the first time this method is called, so we create the
        # special containing node then
        if nest_node.tag == 'submodel':
            created_nest_node = etree.Element('argument', {'name': 'nested_structure',
                                                           'type': 'dictionary'})
        else:
            attrib = {'name': the_id(nest_node),
                      'type': 'dictionary',
                      'parser_action': 'convert_key_to_integer'}
            created_nest_node = etree.Element('nest', attrib)

        if nest_node.find('nest') is not None: # decend into multiple levels of nests
            for nest_child_node in nest_node.findall('nest'):
                child_xml = self._create_nested_structure_xml(nest_child_node, counter)
                created_nest_node.append(child_xml)
        elif nest_node.get('number_of_samples') is not None:
            # auto generate a series of equation ID's
            num_of_samples = int(nest_node.get('number_of_samples'))
            id_start = counter['current count']
            counter['current count'] = id_end = id_start + num_of_samples
            index_range = range(id_start, id_end)
            created_nest_node.set('type', 'list')
            created_nest_node.text = repr(index_range)
        elif nest_node.find('equation') is not None:
            index_range = [int(the_id(x)) for x in nest_node.findall('equation')]
            created_nest_node.set('type', 'list')
            created_nest_node.text = repr(index_range)
        else:
            raise ValueError('Found empty nest without "number_of_samples" attribute or <equation> '
                             'child nodes (%s name=%s)' % (nest_node.tag, nest_node.get('name')))
        return created_nest_node

    def _create_add_variable_menu(self):
        # function to display variables in the popup menu
        def display_node(node, selected_nodes):
            if node in selected_nodes:
                return '(already selected) %s' % get_variable_name(node)
            return get_variable_name(node)
        # call back to only add unselected variables from the popup menu
        def add_if_unselected(node, selected_nodes):
            if not node in selected_nodes:
                self.add_variable(node)

        dataset_variable_nodes = get_variable_nodes_per_dataset(self.project)
        selected_names = map(get_variable_name, self.selector_table_model._variable_nodes)
        selected_nodes = []

        for variable_node_list in dataset_variable_nodes.values():
            for variable_node in variable_node_list:
                if get_variable_name(variable_node) in selected_names:
                    selected_nodes.append(variable_node)
            # sort by variable name
            variable_node_list.sort(lambda x, y: cmp(get_variable_name(x), get_variable_name(y)))
            # display selected items at the bottom
            for node in variable_node_list[:]:
                if node in selected_nodes:
                    variable_node_list.remove(node)
                    variable_node_list.insert(len(variable_node_list), node)

        display_func = lambda x, y = selected_nodes: display_node(x, y)
        callback = lambda x, y = selected_nodes: add_if_unselected(x, y)
        return dictionary_to_menu(source_dict = dataset_variable_nodes,
                                  callback = callback,
                                  display_func = display_func,
                                  parent_widget = self)

    def _right_click_variables(self, point):
        ''' construct and show an operations operations_menu when the user right clicks the variable selector '''
        operations_menu = QtGui.QMenu(self)
        add_variable_menu = self._create_add_variable_menu()
        add_variable_menu.setTitle('Add a variable')
        add_variable_menu.setIcon(IconLibrary.icon('add'))
        operations_menu.addMenu(add_variable_menu)

        index_under_cursor = self.table_selected_variables.indexAt(point)
        if index_under_cursor.isValid():
            row = index_under_cursor.row()
            variable_node = self.selector_table_model.get_variable(row)
            self.table_selected_variables.selectRow(row)
            action = create_qt_action(None, 'Remove %s' % get_variable_name(variable_node),
                                      self._remove_selected_variables,
                                      self.table_selected_variables)
            action.setIcon(IconLibrary.icon('delete'))
            operations_menu.addAction(action)
        else:
            # if the index is not valid -- assume that the user clicked the "white space" of the table
            pass
        operations_menu.exec_(QtGui.QCursor.pos())

    def _in_simple_mode(self):
        return self.tree_structure_selector.topLevelItemCount() == 0

    def _update_available_variables(self):
        # populate the list of available variables while considering A) what dataset filter the user
        # has selected, an d B) what variables that have already been selected)
        self.lst_available_variables.clear()
        if self.cbo_dataset_filter.currentIndex() > 0:
            dataset_filter = str(self.cbo_dataset_filter.currentText())
        else:
            dataset_filter = None
        selected_variable_names = [node.get('name') for node in self.selector_table_model._variable_nodes]

        available_variable_nodes = []
        variable_nodes_per_dataset = get_variable_nodes_per_dataset(self.project)

        if not dataset_filter: # take all variables
            for variable_nodes in variable_nodes_per_dataset.values():
                available_variable_nodes.extend(variable_nodes)
        else:
            available_variable_nodes = variable_nodes_per_dataset[dataset_filter]
        available_variable_nodes.append(get_built_in_constant_node())
        
        # filter already selected variables and show the list of available variables
        not_selected_variables = [var_node for var_node in available_variable_nodes if
                                  not var_node.get('name') in selected_variable_names]
        for variable_node in not_selected_variables:
            item = QtGui.QListWidgetItem(self.lst_available_variables)
            item.setIcon(IconLibrary.icon('variable'))
            item.node = variable_node # monkey in the node for adding later
            item.setText(get_variable_name(variable_node))
            self.lst_available_variables.addItem(item)
        self.lst_available_variables.sortItems()

    def _update_dataset_filter_list(self):
        # update the combo box list (keeping selection)
        variable_nodes_per_dataset = get_variable_nodes_per_dataset(self.project)
        pre_update_dataset_name = self.cbo_dataset_filter.currentText()
        self.cbo_dataset_filter.clear()
        self.cbo_dataset_filter.addItem('[All datasets]')
        for dataset_name in variable_nodes_per_dataset:
            if dataset_name is not None: # built ins are manually added
                self.cbo_dataset_filter.addItem(dataset_name)
        post_update_index = self.cbo_dataset_filter.findText(pre_update_dataset_name)
        if post_update_index > -1:
            self.cbo_dataset_filter.setCurrentIndex(post_update_index)

    def _remove_selected_variables(self):
        # delete the rows from highest to lowest so we don't change the row number when we delete
        # a row (i.e if we want to delete row 1 and 2 and start with one, then the row that was
        # number 2 will have become one and we will actually delete row number 3
        selected_indices = self.table_selected_variables.selectedIndexes()
        selected_rows = list(set([idx.row() for idx in selected_indices]))
        selected_rows.sort(reverse=True)
        for row in selected_rows:
            self.table_selected_variables.model().removeRow(row)
        self._update_available_variables()

    def _show_advanced_parameters(self, set_advanced_visible = None):
        # updates the selector table to show only the basic data if set_advanced_visible is True
        # otherwise all available data is shown
        if set_advanced_visible is None:
            set_advanced_visible = self.cb_show_advanced_parameters.isChecked()
        if set_advanced_visible:
            for column in range(5):
                self.table_selected_variables.showColumn(column)
        else:
            advanced_columns = [0, 2, 3, 4]
            for column in range(5):
                if column in advanced_columns:
                    self.table_selected_variables.hideColumn(column)
                else:
                    self.table_selected_variables.showColumn(column)

    def _set_picker_visible(self, visible):
        # shows / hides the variable picker
        # visible =
        if visible:
            self.stack_struct_picker.setCurrentIndex(0)
            if self._in_simple_mode():
                self.split_struct_variables.setSizes([1, 1])
            # hide unnecessary information while picking variables
            self._show_advanced_parameters(False)
        else:
            self.stack_struct_picker.setCurrentIndex(1)
            if self._in_simple_mode():
                self.split_struct_variables.setSizes([0, 10])
            self._show_advanced_parameters()
            # sometimes the table gets messed up when shown again so force refresh
            self.selector_table_model.emit(QtCore.SIGNAL("layoutChanged()"))
        self.cb_show_advanced_parameters.setEnabled(not visible)
        self.pb_remove_variable.setVisible(visible)
        self.pb_show_picker.setChecked(visible)

    def update_model_nested_structure(self, node = None):
        '''
        Create an XML representation to use for the argument "nested_structure" used by Nested Logit
        Models (NLM). The argument is used in the NLMs init() method.
        @param node submodel node to construct a nested structure from (default self.submodel_node)
        @raise RuntimeError: If the model node could not be updated
        @return the created nested_structure node (useful for tests)
        '''
        node = self.submodel_node if node is None else node
        if node.find('nest') is None: # can't create a nested structure if there are no nests
            return None
        counter = {'current count': 1} # pass object ref to keep increments down the recursive chain
        try:
            new_nested_structure_node = self._create_nested_structure_xml(node, counter)
        except ValueError, ex:
            MessageBox.error(self, 'Not all nests have equations assigned to them.', str(ex))
            return None
        model_node = self._lookup_model_node_for(node)
        if new_nested_structure_node is None or model_node is None:
            err_msg = 'Warning: Could not update the model nested node because:'
            if new_nested_structure_node is None:
                err_msg = err_msg + ' * the created "nested_structure" was empty'
            if model_node is None:
                err_msg = err_msg + ' * a parent <model> node to update could not be found'
            raise RuntimeError(err_msg)

        # replace existing nested_structure(s) with the new one
        init_node = model_node.find('structure/init/')
        for existing_nest_node in init_node.findall("argument[@name='nested_structure']"):
            init_node.remove(existing_nest_node)
        init_node.append(new_nested_structure_node)
        return new_nested_structure_node