예제 #1
0
파일: gui.py 프로젝트: Jakoma02/pyCovering
class MainWindow(QMainWindow, Ui_MainWindow):
    """
    The main GUI window
    """
    HELP_URL = "https://www.github.com/jakoma02/pyCovering"

    model_type_changed = Signal()
    view_type_changed = Signal()
    model_changed = Signal(GeneralCoveringModel)
    view_changed = Signal(GeneralView)
    info_updated = Signal(GeneralCoveringModel, GeneralView)
    settings_changed = Signal()

    def __init__(self):
        QMainWindow.__init__(self)

        self.model = None
        self.view = None

        self.setupUi(self)
        self.create_action_groups()

        # A dict Action name -> GeneralView, so that we can set the
        # correct view upon view type action trigger
        self.action_views = dict()

        self.actionAbout_2.triggered.connect(self.show_about_dialog)
        self.actionDocumentation.triggered.connect(self.show_help)
        self.actionChange_dimensions.triggered.connect(
            self.show_dimensions_dialog)
        self.actionChange_tile_size.triggered.connect(
            self.show_block_size_dialog)

        self.actionGenerate.triggered.connect(self.start_covering)

        self.model_type_changed.connect(self.update_model_type)
        self.model_type_changed.connect(self.update_view_type_menu)
        self.model_type_changed.connect(self.update_constraints_menu)
        self.model_type_changed.connect(self.enable_model_menu_buttons)

        self.model_changed.connect(
            lambda _: self.info_updated.emit(self.model, self.view))

        self.view_type_changed.connect(self.update_view_type)

        self.view_changed.connect(
            lambda _: self.info_updated.emit(self.model, self.view))
        self.info_updated.connect(self.infoText.update)
        self.info_updated.connect(self.update_view)

        self.tiles_list_model = BlockListModel()
        self.tilesList.setModel(self.tiles_list_model)

        self.model_changed.connect(self.tiles_list_model.update_data)
        self.tiles_list_model.checkedChanged.connect(self.set_block_visibility)

        self.model_changed.emit(self.model)
        self.update_view_type_menu()

    def set_block_visibility(self, block, visible):
        """
        Update model visibility based on block list checkbox change
        """
        block.visible = visible
        self.model_changed.emit(self.model)

    def show_about_dialog(self):
        """
        Shows the "About" dialog
        """
        dialog = AboutDialog(self)
        dialog.open()

    def start_covering(self):
        """
        Starts covering, shows the corresponding dialog
        """
        if self.model is None:
            QMessageBox.warning(self, "No model", "No model selected!")
            return

        self.model.reset()

        self.thread = GenerateModelThread(self.model)
        dialog = CoveringDialog(self)

        dialog.rejected.connect(self.cancel_covering)

        self.thread.success.connect(dialog.accept)
        self.thread.success.connect(self.covering_success)
        self.thread.failed.connect(dialog.reject)
        self.thread.failed.connect(self.covering_failed)

        self.thread.done.connect(lambda: self.model_changed.emit(self.model))

        self.thread.start()
        dialog.open()

    def show_block_size_dialog(self):
        """
        Shows "Change block size" dialog
        """
        if self.model is None:
            QMessageBox.warning(self, "No model", "No model selected!")
            return

        curr_min = self.model.min_block_size
        curr_max = self.model.max_block_size

        dialog = BlockSizeDialog(self)
        dialog.sizesAccepted.connect(self.block_sizes_accepted)
        dialog.set_values(curr_min, curr_max)
        dialog.open()

    def show_dimensions_dialog(self):
        """
        Shows "Change dimensions" dialog
        """
        if self.model is None:
            QMessageBox.warning(self, "No model", "No model selected!")
            return

        if isinstance(self.model, TwoDCoveringModel):
            curr_width = self.model.width
            curr_height = self.model.height

            dialog = TwoDDimensionsDialog(self)
            dialog.set_values(curr_width, curr_height)

            dialog.dimensionsAccepted.connect(self.two_d_dimensions_accepted)
            dialog.show()

        elif isinstance(self.model, PyramidCoveringModel):
            curr_size = self.model.size

            dialog = PyramidDimensionsDialog(self)
            dialog.set_value(curr_size)

            dialog.dimensionsAccepted.connect(self.pyramid_dimensions_accepted)
            dialog.show()

    def two_d_dimensions_accepted(self, width, height):
        """
        Updates TwoDCoveringModel dimensions (after dialog confirmation)
        """
        assert isinstance(self.model, TwoDCoveringModel)

        self.model.set_size(width, height)
        self.model_changed.emit(self.model)

        self.message("Size updated")

    def pyramid_dimensions_accepted(self, size):
        """
        Updates PyramidCoveringModel dimensions (after dialog confirmation)
        """
        assert isinstance(self.model, PyramidCoveringModel)

        # PyLint doesn't know that this is a `PyramidCoveringModel`
        # and not a `TwoDCoveringModel`
        # pylint: disable=no-value-for-parameter
        self.model.set_size(size)
        self.model_changed.emit(self.model)

        self.message("Size updated")

    def block_sizes_accepted(self, min_val, max_val):
        """
        Updates covering model block size (after dialog confirmation)
        """
        assert self.model is not None

        self.model.set_block_size(min_val, max_val)
        self.model_changed.emit(self.model)

        self.message("Block size updated")

    @staticmethod
    def update_view(model, view):
        """
        Refreshes contents of given view
        """
        if view is None:
            return
        if model is not None and model.is_filled():
            view.show(model)
        else:
            view.close()

    def message(self, msg):
        """
        Shows a log message in the "Messages" window
        """
        self.messagesText.add_message(msg)

    def show_help(self):
        """
        Opens a webpage with help
        """
        webbrowser.open(self.HELP_URL)

    def create_action_groups(self):
        """
        Groups exclusive choice menu buttons in action groups.

        This should ideally be done in UI files, but Qt designer
        doesn't support it.
        """
        self.model_type_group = QActionGroup(self)
        self.model_type_group.addAction(self.action2D_Rectangle_2)
        self.model_type_group.addAction(self.actionPyramid_2)

        self.view_type_group = QActionGroup(self)

        self.model_type_group.triggered.connect(self.model_type_changed)
        self.view_type_group.triggered.connect(self.view_type_changed)

    def update_model_type(self):
        """
        Sets the current model after model type changed in menu
        """
        selected_model = self.model_type_group.checkedAction()

        if selected_model == self.action2D_Rectangle_2:
            model = TwoDCoveringModel(10, 10, 4, 4)
        elif selected_model == self.actionPyramid_2:
            model = PyramidCoveringModel(10, 4, 4)
        else:
            model = None

        self.model = model
        self.model_changed.emit(model)
        self.message("Model type updated")

    def enable_model_menu_buttons(self):
        """
        Enable menu buttons that are disabled at program start
        """
        self.actionChange_dimensions.setEnabled(True)
        self.actionChange_tile_size.setEnabled(True)
        self.actionGenerate.setEnabled(True)

    def update_view_type(self):
        """
        Sets the current view after view type changed in menu
        """
        if self.view is not None:
            self.view.close()

        selected_action = self.view_type_group.checkedAction()

        if selected_action is None:
            # Model was probably changed
            self.view = None
        else:
            action_name = selected_action.objectName()
            selected_view = self.action_views[action_name]

            self.view = selected_view()  # New instance of that view

            self.message("View type updated")

        self.view_changed.emit(self.view)

    def cancel_covering(self):
        """
        Stops ongoing covering
        """
        if self.thread.isRunning():
            # The thread is being terminated
            self.model.stop_covering()
            self.message("Covering terminated")

    def covering_success(self):
        """
        Prints a success log message (for now)
        """
        self.message("Covering successful")

    def covering_failed(self):
        """
        Prints a fail log message and shows an error window (for now)
        """
        self.message("Covering failed")
        QMessageBox.critical(self, "Failed", "Covering failed")

    def model_views(self, model):
        """
        Returns a list of tuples for all views
        for given mode as  (name, class)
        """

        if isinstance(model, TwoDCoveringModel):
            return [
                ("2D Print view", text_view_decorator(TwoDPrintView, self)),
                ("2D Visual view", parented_decorator(TwoDVisualView, self))
            ]

        if isinstance(model, PyramidCoveringModel):
            return [("Pyramid Print view",
                     text_view_decorator(PyramidPrintView, self)),
                    ("Pyramid Visual view", PyramidVisualView)]

        return []

    @staticmethod
    def model_constraints(model):
        """
        Returns a list of tuples for all constraint watchers
        for given mode as  (name, class)
        """

        if isinstance(model, TwoDCoveringModel):
            return [("Path blocks", PathConstraintWatcher)]

        if isinstance(model, PyramidCoveringModel):
            return [("Path blocks", PathConstraintWatcher),
                    ("Planar blocks", PlanarConstraintWatcher)]

        return []

    def update_view_type_menu(self):
        """
        Updates options for view type menu afted model type change
        """
        view_type_menu = self.menuType_2
        view_type_menu.clear()

        for action in self.view_type_group.actions():
            self.view_type_group.removeAction(action)

        all_views = self.model_views(self.model)

        if not all_views:
            # Likely no model selected
            view_type_menu.setEnabled(False)
            return

        view_type_menu.setEnabled(True)

        self.action_views.clear()

        for i, view_tuple in enumerate(all_views):
            name, view = view_tuple

            # As good as any, we just need to distinguish the actions
            action_name = f"Action{i}"

            action = QAction(self)
            action.setText(name)
            action.setCheckable(True)
            action.setObjectName(action_name)

            # So that we can later see which view should be activated
            self.action_views[action_name] = view
            view_type_menu.addAction(action)
            self.view_type_group.addAction(action)

        self.update_view_type()

    def watcher_set_active(self, constraint, value):
        """
        A slot, activate/deactivate constraint depending on value (True/False)
        """

        if value is True:
            self.model.add_constraint(constraint)
        else:
            self.model.remove_constraint(constraint)

        self.model_changed.emit(self.model)
        self.message("Constraint settings changed")

    def update_constraints_menu(self):
        """
        Updates options for model constraints after model type change
        """

        cstr_menu = self.menuConstraints
        cstr_menu.clear()

        all_constraints = self.model_constraints(self.model)

        for name, watcher in all_constraints:
            action = QAction(self)
            action.setText(name)
            action.setCheckable(True)

            action.toggled.connect(lambda val, watcher=watcher: self.
                                   watcher_set_active(watcher, val))

            cstr_menu.addAction(action)

        cstr_menu.setEnabled(True)

    def close(self):
        """
        While closing the window also closes the view
        """
        if self.view is not None:
            self.view.close()

        super().close()
예제 #2
0
class TabularViewMixin:
    """Provides the pivot table and its frozen table for the DS form."""

    _PARAMETER_VALUE = "&Value"
    _INDEX_EXPANSION = "&Index"
    _RELATIONSHIP = "Re&lationship"
    _SCENARIO_ALTERNATIVE = "&Scenario"

    _PARAMETER = "parameter"
    _ALTERNATIVE = "alternative"
    _INDEX = "index"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._pending_index = None
        # current state of ui
        self.current_class_item = None  # Current QModelIndex selected in one of the entity tree views
        self.current_class_type = None
        self.current_class_id = {}  # Mapping from db_map to class_id
        self.current_class_name = None
        self.current_input_type = self._PARAMETER_VALUE
        self.filter_menus = {}
        self.class_pivot_preferences = {}
        self.PivotPreferences = namedtuple(
            "PivotPreferences", ["index", "columns", "frozen", "frozen_value"])
        self.pivot_action_group = QActionGroup(self)
        self.populate_pivot_action_group()
        self.pivot_table_proxy = PivotTableSortFilterProxy()
        self.pivot_table_model = None
        self.frozen_table_model = FrozenTableModel(self)
        self.ui.pivot_table.setModel(self.pivot_table_proxy)
        self.ui.pivot_table.connect_spine_db_editor(self)
        self.ui.frozen_table.setModel(self.frozen_table_model)
        self.ui.frozen_table.verticalHeader().setDefaultSectionSize(
            self.default_row_height)

    def populate_pivot_action_group(self):
        actions = {
            input_type:
            self.pivot_action_group.addAction(QIcon(CharIconEngine(icon_code)),
                                              input_type)
            for input_type, icon_code in (
                (self._PARAMETER_VALUE, "\uf292"),
                (self._INDEX_EXPANSION, "\uf12c"),
                (self._RELATIONSHIP, "\uf1b3"),
                (self._SCENARIO_ALTERNATIVE, "\uf008"),
            )
        }
        for action in actions.values():
            action.setCheckable(True)
        actions[self.current_input_type].setChecked(True)

    def connect_signals(self):
        """Connects signals to slots."""
        super().connect_signals()
        self.ui.pivot_table.horizontalHeader().header_dropped.connect(
            self.handle_header_dropped)
        self.ui.pivot_table.verticalHeader().header_dropped.connect(
            self.handle_header_dropped)
        self.ui.frozen_table.header_dropped.connect(self.handle_header_dropped)
        self.ui.frozen_table.selectionModel().currentChanged.connect(
            self.change_frozen_value)
        self.pivot_action_group.triggered.connect(self.do_reload_pivot_table)
        self.ui.dockWidget_pivot_table.visibilityChanged.connect(
            self._handle_pivot_table_visibility_changed)
        self.ui.dockWidget_frozen_table.visibilityChanged.connect(
            self._handle_frozen_table_visibility_changed)

    def init_models(self):
        """Initializes models."""
        super().init_models()
        self.clear_pivot_table()

    @Slot("QModelIndex", object)
    def _set_model_data(self, index, value):
        self.pivot_table_proxy.setData(index, value)

    @property
    def current_object_class_id_list(self):
        if self.current_class_type == "object_class":
            return [self.current_class_id]
        current_object_class_id_list = [
            {} for _ in self.current_object_class_name_list
        ]
        for db_map, class_id in self.current_class_id.items():
            relationship_class = self.db_mngr.get_item(db_map,
                                                       "relationship_class",
                                                       class_id)
            for k, id_ in enumerate(
                    relationship_class["object_class_id_list"].split(",")):
                current_object_class_id_list[k][db_map] = int(id_)
        return current_object_class_id_list

    @property
    def current_object_class_name_list(self):
        db_map, class_id = next(iter(self.current_class_id.items()))
        if self.current_class_type == "object_class":
            return [
                self.db_mngr.get_item(db_map, "object_class", class_id)["name"]
            ]
        relationship_class = self.db_mngr.get_item(db_map,
                                                   "relationship_class",
                                                   class_id)
        return fix_name_ambiguity(
            relationship_class["object_class_name_list"].split(","))

    @property
    def current_object_class_ids(self):
        return dict(
            zip(self.current_object_class_name_list,
                self.current_object_class_id_list))

    @staticmethod
    def _is_class_index(index):
        """Returns whether or not the given tree index is a class index.

        Args:
            index (QModelIndex): index from object or relationship tree
        Returns:
            bool
        """
        return index.column() == 0 and not index.parent().parent().isValid()

    @Slot(bool)
    def _handle_pivot_table_visibility_changed(self, visible):
        if not visible:
            return
        self.ui.dockWidget_frozen_table.setVisible(True)
        if self._pending_index is not None:
            QTimer.singleShot(
                100, lambda: self.reload_pivot_table(self._pending_index))

    @Slot(bool)
    def _handle_frozen_table_visibility_changed(self, visible):
        if visible:
            self.ui.dockWidget_pivot_table.show()

    @Slot(dict)
    def _handle_object_tree_selection_changed(self, selected_indexes):
        super()._handle_object_tree_selection_changed(selected_indexes)
        current = self.ui.treeView_object.currentIndex()
        self._handle_entity_tree_current_changed(current)

    @Slot(dict)
    def _handle_relationship_tree_selection_changed(self, selected_indexes):
        super()._handle_relationship_tree_selection_changed(selected_indexes)
        current = self.ui.treeView_relationship.currentIndex()
        self._handle_entity_tree_current_changed(current)

    def _handle_entity_tree_current_changed(self, current_index):
        if self.current_input_type == self._SCENARIO_ALTERNATIVE:
            return
        if not self.ui.dockWidget_pivot_table.isVisible():
            self._pending_index = current_index
            return
        self.reload_pivot_table(current_index=current_index)

    @staticmethod
    def _make_get_id(action):
        """Returns a function to compute the db_map-id tuple of an item."""
        return {
            "add": lambda db_map, x: (db_map, x["id"]),
            "remove": lambda db_map, x: None
        }[action]

    def _get_db_map_entities(self):
        """Returns a dict mapping db maps to a list of dict entity items in the current class.

        Returns:
            dict
        """
        entity_type = {
            "object_class": "object",
            "relationship_class": "relationship"
        }[self.current_class_type]
        return {
            db_map: self.db_mngr.get_items_by_field(db_map, entity_type,
                                                    "class_id", class_id)
            for db_map, class_id in self.current_class_id.items()
        }

    def load_empty_relationship_data(self, db_map_class_objects=None):
        """Returns a dict containing all possible relationships in the current class.

        Args:
            db_map_class_objects (dict)

        Returns:
            dict: Key is db_map-object_id tuple, value is None.
        """
        if db_map_class_objects is None:
            db_map_class_objects = dict()
        if self.current_class_type == "object_class":
            return {}
        data = {}
        for db_map in self.db_maps:
            object_id_lists = []
            all_given_ids = set()
            for db_map_class_id in self.current_object_class_id_list:
                class_id = db_map_class_id.get(db_map)
                objects = self.db_mngr.get_items_by_field(
                    db_map, "object", "class_id", class_id)
                ids = {item["id"]: None for item in objects}
                given_objects = db_map_class_objects.get(db_map,
                                                         {}).get(class_id)
                if given_objects is not None:
                    given_ids = {item["id"]: None for item in given_objects}
                    ids.update(given_ids)
                    all_given_ids.update(given_ids.keys())
                object_id_lists.append(list(ids.keys()))
            db_map_data = {
                tuple((db_map, id_) for id_ in objects_ids) + (db_map, ): None
                for objects_ids in product(*object_id_lists)
                if not all_given_ids or all_given_ids.intersection(objects_ids)
            }
            data.update(db_map_data)
        return data

    def load_full_relationship_data(self,
                                    db_map_relationships=None,
                                    action="add"):
        """Returns a dict of relationships in the current class.

        Args:
            db_map_relationships (dict)

        Returns:
            dict: Key is db_map-object id tuple, value is relationship id.
        """
        if self.current_class_type == "object_class":
            return {}
        if db_map_relationships is None:
            db_map_relationships = self._get_db_map_entities()
        get_id = self._make_get_id(action)
        return {
            tuple((db_map, int(id_))
                  for id_ in rel["object_id_list"].split(',')) + (db_map, ):
            get_id(db_map, rel)
            for db_map, relationships in db_map_relationships.items()
            for rel in relationships
        }

    def load_relationship_data(self):
        """Returns a dict that merges empty and full relationship data.

        Returns:
            dict: Key is object id tuple, value is True if a relationship exists, False otherwise.
        """
        data = self.load_empty_relationship_data()
        data.update(self.load_full_relationship_data())
        return data

    def load_scenario_alternative_data(self,
                                       db_map_scenarios=None,
                                       db_map_alternatives=None):
        """Returns a dict containing all scenario alternatives.

        Returns:
            dict: Key is db_map-id tuple, value is None or rank.
        """
        if db_map_scenarios is None:
            db_map_scenarios = {
                db_map: self.db_mngr.get_items(db_map, "scenario")
                for db_map in self.db_maps
            }
        if db_map_alternatives is None:
            db_map_alternatives = {
                db_map: self.db_mngr.get_items(db_map, "alternative")
                for db_map in self.db_maps
            }
        data = {}
        for db_map in self.db_maps:
            scenario_alternative_ranks = {
                x["id"]: {
                    alt_id: k + 1
                    for k, alt_id in enumerate(
                        self.db_mngr.get_scenario_alternative_id_list(
                            db_map, x["id"]))
                }
                for x in db_map_scenarios.get(db_map, [])
            }
            alternative_ids = [
                x["id"] for x in db_map_alternatives.get(db_map, [])
            ]
            db_map_data = {((db_map, scen_id), (db_map, alt_id), db_map):
                           alternative_ranks.get(alt_id)
                           for scen_id, alternative_ranks in
                           scenario_alternative_ranks.items()
                           for alt_id in alternative_ids}
            data.update(db_map_data)
        return data

    def _get_parameter_value_or_def_ids(self, item_type):
        """Returns a dict mapping db maps to a list of integer parameter (value or def) ids from the current class.

        Args:
            item_type (str): either "parameter_value" or "parameter_definition"

        Returns:
            dict
        """
        class_id_field = {
            "object_class": "object_class_id",
            "relationship_class": "relationship_class_id"
        }[self.current_class_type]
        return {
            db_map: [
                x["id"] for x in self.db_mngr.get_items_by_field(
                    db_map, item_type, class_id_field, class_id)
            ]
            for db_map, class_id in self.current_class_id.items()
        }

    def _get_db_map_parameter_values_or_defs(self, item_type):
        """Returns a dict mapping db maps to list of dict parameter (value or def) items from the current class.

        Args:
            item_type (str): either "parameter_value" or "parameter_definition"

        Returns:
            dict
        """
        db_map_ids = self._get_parameter_value_or_def_ids(item_type)
        return {
            db_map:
            [self.db_mngr.get_item(db_map, item_type, id_) for id_ in ids]
            for db_map, ids in db_map_ids.items()
        }

    def load_empty_parameter_value_data(self,
                                        db_map_entities=None,
                                        db_map_parameter_ids=None,
                                        db_map_alternative_ids=None):
        """Returns a dict containing all possible combinations of entities and parameters for the current class
        in all db_maps.

        Args:
            db_map_entities (dict, optional): if given, only load data for these db maps and entities
            db_map_parameter_ids (dict, optional): if given, only load data for these db maps and parameter definitions
            db_map_alternative_ids (dict, optional): if given, only load data for these db maps and alternatives

        Returns:
            dict: Key is a tuple object_id, ..., parameter_id, value is None.
        """
        if db_map_entities is None:
            db_map_entities = self._get_db_map_entities()
        if db_map_parameter_ids is None:
            db_map_parameter_ids = {
                db_map: [(db_map, id_) for id_ in ids]
                for db_map, ids in self._get_parameter_value_or_def_ids(
                    "parameter_definition").items()
            }
        if db_map_alternative_ids is None:
            db_map_alternative_ids = {
                db_map:
                [(db_map, a["id"])
                 for a in self.db_mngr.get_items(db_map, "alternative")]
                for db_map in self.db_maps
            }
        if self.current_class_type == "relationship_class":
            db_map_entity_ids = {
                db_map: [
                    tuple((db_map, int(id_))
                          for id_ in e["object_id_list"].split(','))
                    for e in entities
                ]
                for db_map, entities in db_map_entities.items()
            }
        else:
            db_map_entity_ids = {
                db_map: [((db_map, e["id"]), ) for e in entities]
                for db_map, entities in db_map_entities.items()
            }
        if not db_map_entity_ids:
            db_map_entity_ids = {
                db_map: [
                    tuple((db_map, None)
                          for _ in self.current_object_class_id_list)
                ]
                for db_map in self.db_maps
            }
        if not db_map_parameter_ids:
            db_map_parameter_ids = {
                db_map: [(db_map, None)]
                for db_map in self.db_maps
            }
        return {
            entity_id + (parameter_id, alt_id, db_map): None
            for db_map in self.db_maps
            for entity_id in db_map_entity_ids.get(db_map, [])
            for parameter_id in db_map_parameter_ids.get(db_map, [])
            for alt_id in db_map_alternative_ids.get(db_map, [])
        }

    def load_full_parameter_value_data(self,
                                       db_map_parameter_values=None,
                                       action="add"):
        """Returns a dict of parameter values for the current class.

        Args:
            db_map_parameter_values (list, optional)
            action (str)

        Returns:
            dict: Key is a tuple object_id, ..., parameter_id, value is the parameter_value.
        """
        if db_map_parameter_values is None:
            db_map_parameter_values = self._get_db_map_parameter_values_or_defs(
                "parameter_value")
        get_id = self._make_get_id(action)
        if self.current_class_type == "object_class":
            return {((db_map, x["object_id"]), (db_map, x["parameter_id"]),
                     (db_map, x["alternative_id"]), db_map): get_id(db_map, x)
                    for db_map, items in db_map_parameter_values.items()
                    for x in items}
        return {
            tuple((db_map, int(id_))
                  for id_ in x["object_id_list"].split(',')) +
            ((db_map, x["parameter_id"]),
             (db_map, x["alternative_id"]), db_map): get_id(db_map, x)
            for db_map, items in db_map_parameter_values.items() for x in items
        }

    def _indexes(self, value):
        if value is None:
            return []
        db_map, id_ = value
        return self.db_mngr.get_value_indexes(db_map, "parameter_value", id_)

    def load_empty_expanded_parameter_value_data(self,
                                                 db_map_entities=None,
                                                 db_map_parameter_ids=None,
                                                 db_map_alternative_ids=None):
        """Makes a dict of expanded parameter values for the current class.

        Args:
            db_map_parameter_values (list, optional)
            action (str)

        Returns:
            dict: mapping from unique value id tuple to value tuple
        """
        data = self.load_empty_parameter_value_data(db_map_entities,
                                                    db_map_parameter_ids,
                                                    db_map_alternative_ids)
        return {
            key[:-3] + ((None, index), ) + key[-3:]: value
            for key, value in data.items() for index in self._indexes(value)
        }

    def load_full_expanded_parameter_value_data(self,
                                                db_map_parameter_values=None,
                                                action="add"):
        """Makes a dict of expanded parameter values for the current class.

        Args:
            db_map_parameter_values (list, optional)
            action (str)

        Returns:
            dict: mapping from unique value id tuple to value tuple
        """
        data = self.load_full_parameter_value_data(db_map_parameter_values,
                                                   action)
        return {
            key[:-3] + ((None, index), ) + key[-3:]: value
            for key, value in data.items() for index in self._indexes(value)
        }

    def load_parameter_value_data(self):
        """Returns a dict that merges empty and full parameter_value data.

        Returns:
            dict: Key is a tuple object_id, ..., parameter_id, value is the parameter_value or None if not specified.
        """
        data = self.load_empty_parameter_value_data()
        data.update(self.load_full_parameter_value_data())
        return data

    def load_expanded_parameter_value_data(self):
        """
        Returns all permutations of entities as well as parameter indexes and values for the current class.

        Returns:
            dict: Key is a tuple object_id, ..., index, while value is None.
        """
        data = self.load_empty_expanded_parameter_value_data()
        data.update(self.load_full_expanded_parameter_value_data())
        return data

    def get_pivot_preferences(self):
        """Returns saved pivot preferences.

        Returns:
            tuple, NoneType: pivot tuple, or None if no preference stored
        """
        selection_key = (self.current_class_name, self.current_class_type,
                         self.current_input_type)
        if selection_key in self.class_pivot_preferences:
            rows = self.class_pivot_preferences[selection_key].index
            columns = self.class_pivot_preferences[selection_key].columns
            frozen = self.class_pivot_preferences[selection_key].frozen
            frozen_value = self.class_pivot_preferences[
                selection_key].frozen_value
            return (rows, columns, frozen, frozen_value)
        return None

    def reload_pivot_table(self, current_index=None):
        """Updates current class (type and id) and reloads pivot table for it."""
        self._pending_index = None
        if current_index is not None:
            self.current_class_item = self._get_current_class_item(
                current_index)
        if self.current_class_item is None:
            self.current_class_id = {}
            self.clear_pivot_table()
            return
        class_id = self.current_class_item.db_map_ids
        if self.current_class_id == class_id:
            return
        self.clear_pivot_table()
        self.current_class_type = self.current_class_item.item_type
        self.current_class_id = class_id
        self.current_class_name = self.current_class_item.display_data
        self.do_reload_pivot_table()

    @staticmethod
    def _get_current_class_item(current_index):
        item = current_index.model().item_from_index(current_index)
        while item.item_type != "root":
            if item.item_type in ("object_class", "relationship_class"):
                return item
            item = item.parent_item
        return None

    @busy_effect
    @Slot("QAction")
    def do_reload_pivot_table(self, action=None):
        """Reloads pivot table.
        """
        qApp.processEvents()  # pylint: disable=undefined-variable
        if action is None:
            action = self.pivot_action_group.checkedAction()
        self.current_input_type = action.text()
        if not self._can_build_pivot_table():
            return
        self.pivot_table_model = {
            self._PARAMETER_VALUE: ParameterValuePivotTableModel,
            self._RELATIONSHIP: RelationshipPivotTableModel,
            self._INDEX_EXPANSION: IndexExpansionPivotTableModel,
            self._SCENARIO_ALTERNATIVE: ScenarioAlternativePivotTableModel,
        }[self.current_input_type](self)
        self.pivot_table_proxy.setSourceModel(self.pivot_table_model)
        delegate = self.pivot_table_model.make_delegate(self)
        self.ui.pivot_table.setItemDelegate(delegate)
        self.pivot_table_model.modelReset.connect(self.make_pivot_headers)
        pivot = self.get_pivot_preferences()
        self.wipe_out_filter_menus()
        self.pivot_table_model.call_reset_model(pivot)
        self.pivot_table_proxy.clear_filter()
        self.reload_frozen_table()

    def _can_build_pivot_table(self):
        if self.current_input_type != self._SCENARIO_ALTERNATIVE and not self.current_class_id:
            return False
        if self.current_input_type == self._RELATIONSHIP and self.current_class_type != "relationship_class":
            return False
        return True

    def clear_pivot_table(self):
        self.wipe_out_filter_menus()
        if self.pivot_table_model:
            self.pivot_table_model.clear_model()
            self.pivot_table_proxy.clear_filter()
        if self.frozen_table_model:
            self.frozen_table_model.clear_model()

    def wipe_out_filter_menus(self):
        while self.filter_menus:
            _, menu = self.filter_menus.popitem()
            menu.wipe_out()

    @Slot()
    def make_pivot_headers(self):
        """
        Turns top left indexes in the pivot table into TabularViewHeaderWidget.
        """
        top_indexes, left_indexes = self.pivot_table_model.top_left_indexes()
        for index in left_indexes:
            proxy_index = self.pivot_table_proxy.mapFromSource(index)
            widget = self.create_header_widget(
                proxy_index.data(Qt.DisplayRole), "columns")
            self.ui.pivot_table.setIndexWidget(proxy_index, widget)
        for index in top_indexes:
            proxy_index = self.pivot_table_proxy.mapFromSource(index)
            widget = self.create_header_widget(
                proxy_index.data(Qt.DisplayRole), "rows")
            self.ui.pivot_table.setIndexWidget(proxy_index, widget)
        QTimer.singleShot(0, self._resize_pivot_header_columns)

    @Slot()
    def _resize_pivot_header_columns(self):
        top_indexes, _ = self.pivot_table_model.top_left_indexes()
        for index in top_indexes:
            self.ui.pivot_table.resizeColumnToContents(index.column())

    def make_frozen_headers(self):
        """
        Turns indexes in the first row of the frozen table into TabularViewHeaderWidget.
        """
        for column in range(self.frozen_table_model.columnCount()):
            index = self.frozen_table_model.index(0, column)
            widget = self.create_header_widget(index.data(Qt.DisplayRole),
                                               "frozen",
                                               with_menu=False)
            self.ui.frozen_table.setIndexWidget(index, widget)
            column_width = self.ui.frozen_table.horizontalHeader().sectionSize(
                column)
            header_width = widget.size().width()
            width = max(column_width, header_width)
            self.ui.frozen_table.horizontalHeader().resizeSection(
                column, width)

    def create_filter_menu(self, identifier):
        """Returns a filter menu for given given object_class identifier.

        Args:
            identifier (int)

        Returns:
            TabularViewFilterMenu
        """
        if identifier not in self.filter_menus:
            pivot_top_left_header = self.pivot_table_model.top_left_headers[
                identifier]
            data_to_value = pivot_top_left_header.header_data
            self.filter_menus[identifier] = menu = TabularViewFilterMenu(
                self, identifier, data_to_value, show_empty=False)
            index_values = dict.fromkeys(
                self.pivot_table_model.model.index_values.get(identifier, []))
            index_values.pop(None, None)
            menu.set_filter_list(index_values.keys())
            menu.filterChanged.connect(self.change_filter)
        return self.filter_menus[identifier]

    def create_header_widget(self, identifier, area, with_menu=True):
        """
        Returns a TabularViewHeaderWidget for given object_class identifier.

        Args:
            identifier (str)
            area (str)
            with_menu (bool)

        Returns:
            TabularViewHeaderWidget
        """
        menu = self.create_filter_menu(identifier) if with_menu else None
        widget = TabularViewHeaderWidget(identifier,
                                         area,
                                         menu=menu,
                                         parent=self)
        widget.header_dropped.connect(self.handle_header_dropped)
        return widget

    @staticmethod
    def _get_insert_index(pivot_list, catcher, position):
        """Returns an index for inserting a new element in the given pivot list.

        Returns:
            int
        """
        if isinstance(catcher, TabularViewHeaderWidget):
            i = pivot_list.index(catcher.identifier)
            if position == "after":
                i += 1
        else:
            i = 0
        return i

    @Slot(object, object, str)
    def handle_header_dropped(self, dropped, catcher, position=""):
        """
        Updates pivots when a header is dropped.

        Args:
            dropped (TabularViewHeaderWidget)
            catcher (TabularViewHeaderWidget, PivotTableHeaderView, FrozenTableView)
            position (str): either "before", "after", or ""
        """
        top_indexes, left_indexes = self.pivot_table_model.top_left_indexes()
        rows = [index.data(Qt.DisplayRole) for index in top_indexes]
        columns = [index.data(Qt.DisplayRole) for index in left_indexes]
        frozen = self.frozen_table_model.headers
        dropped_list = {
            "columns": columns,
            "rows": rows,
            "frozen": frozen
        }[dropped.area]
        catcher_list = {
            "columns": columns,
            "rows": rows,
            "frozen": frozen
        }[catcher.area]
        dropped_list.remove(dropped.identifier)
        i = self._get_insert_index(catcher_list, catcher, position)
        catcher_list.insert(i, dropped.identifier)
        if dropped.area == "frozen" or catcher.area == "frozen":
            if frozen:
                frozen_values = self.find_frozen_values(frozen)
                self.frozen_table_model.reset_model(frozen_values, frozen)
                self.ui.frozen_table.resizeColumnsToContents()
                self.make_frozen_headers()
            else:
                self.frozen_table_model.clear_model()
        frozen_value = self.get_frozen_value(
            self.ui.frozen_table.currentIndex())
        self.pivot_table_model.set_pivot(rows, columns, frozen, frozen_value)
        # save current pivot
        self.class_pivot_preferences[(
            self.current_class_name, self.current_class_type,
            self.current_input_type)] = self.PivotPreferences(
                rows, columns, frozen, frozen_value)
        self.make_pivot_headers()

    def get_frozen_value(self, index):
        """
        Returns the value in the frozen table corresponding to the given index.

        Args:
            index (QModelIndex)
        Returns:
            tuple
        """
        if not index.isValid():
            return tuple(None
                         for _ in range(self.frozen_table_model.columnCount()))
        return self.frozen_table_model.row(index)

    @Slot("QModelIndex", "QModelIndex")
    def change_frozen_value(self, current, previous):
        """Sets the frozen value from selection in frozen table.
        """
        frozen_value = self.get_frozen_value(current)
        self.pivot_table_model.set_frozen_value(frozen_value)
        # store pivot preferences
        self.class_pivot_preferences[(
            self.current_class_name, self.current_class_type,
            self.current_input_type)] = self.PivotPreferences(
                self.pivot_table_model.model.pivot_rows,
                self.pivot_table_model.model.pivot_columns,
                self.pivot_table_model.model.pivot_frozen,
                self.pivot_table_model.model.frozen_value,
            )

    @Slot(str, set, bool)
    def change_filter(self, identifier, valid_values, has_filter):
        if has_filter:
            self.pivot_table_proxy.set_filter(identifier, valid_values)
        else:
            self.pivot_table_proxy.set_filter(
                identifier, None)  # None means everything passes

    def reload_frozen_table(self):
        """Resets the frozen model according to new selection in entity trees."""
        if not self.pivot_table_model:
            return
        frozen = self.pivot_table_model.model.pivot_frozen
        frozen_value = self.pivot_table_model.model.frozen_value
        frozen_values = self.find_frozen_values(frozen)
        self.frozen_table_model.reset_model(frozen_values, frozen)
        self.ui.frozen_table.resizeColumnsToContents()
        self.make_frozen_headers()
        if frozen_value in frozen_values:
            # update selected row
            ind = frozen_values.index(frozen_value)
            self.ui.frozen_table.selectionModel().blockSignals(
                True)  # prevent selectionChanged signal when updating
            self.ui.frozen_table.selectRow(ind + 1)
            self.ui.frozen_table.selectionModel().blockSignals(False)
        else:
            # frozen value not found, remove selection
            self.ui.frozen_table.selectionModel().blockSignals(
                True)  # prevent selectionChanged signal when updating
            self.ui.frozen_table.clearSelection()
            self.ui.frozen_table.selectionModel().blockSignals(False)

    def find_frozen_values(self, frozen):
        """Returns a list of tuples containing unique values (object ids) for the frozen indexes (object_class ids).

        Args:
            frozen (tuple(int)): A tuple of currently frozen indexes
        Returns:
            list(tuple(list(int)))
        """
        return list(
            dict.fromkeys(
                zip(*[
                    self.pivot_table_model.model.index_values.get(k, [])
                    for k in frozen
                ])).keys())

    # TODO: Move this to the models?
    @staticmethod
    def refresh_table_view(table_view):
        top_left = table_view.indexAt(table_view.rect().topLeft())
        bottom_right = table_view.indexAt(table_view.rect().bottomRight())
        if not bottom_right.isValid():
            model = table_view.model()
            bottom_right = table_view.model().index(model.rowCount() - 1,
                                                    model.columnCount() - 1)
        table_view.model().dataChanged.emit(top_left, bottom_right)

    @Slot(str)
    def update_filter_menus(self, action):
        for identifier, menu in self.filter_menus.items():
            index_values = dict.fromkeys(
                self.pivot_table_model.model.index_values.get(identifier, []))
            index_values.pop(None, None)
            if action == "add":
                menu.add_items_to_filter_list(list(index_values.keys()))
            elif action == "remove":
                previous = menu._filter._filter_model._data_set
                menu.remove_items_from_filter_list(
                    list(previous - index_values.keys()))
        self.reload_frozen_table()

    def receive_objects_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        if self.pivot_table_model.receive_objects_added_or_removed(
                db_map_data, action):
            self.update_filter_menus(action)

    def receive_relationships_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        if self.pivot_table_model.receive_relationships_added_or_removed(
                db_map_data, action):
            self.update_filter_menus(action)

    def receive_parameter_definitions_added_or_removed(self, db_map_data,
                                                       action):
        if not self.pivot_table_model:
            return
        if self.pivot_table_model.receive_parameter_definitions_added_or_removed(
                db_map_data, action):
            self.update_filter_menus(action)

    def receive_alternatives_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        if self.pivot_table_model.receive_alternatives_added_or_removed(
                db_map_data, action):
            self.update_filter_menus(action)

    def receive_parameter_values_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        if self.pivot_table_model.receive_parameter_values_added_or_removed(
                db_map_data, action):
            self.update_filter_menus(action)

    def receive_scenarios_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        if self.pivot_table_model.receive_scenarios_added_or_removed(
                db_map_data, action):
            self.update_filter_menus(action)

    def receive_db_map_data_updated(self, db_map_data, get_class_id):
        if not self.pivot_table_model:
            return
        for db_map, items in db_map_data.items():
            for item in items:
                if get_class_id(item) == self.current_class_id.get(db_map):
                    self.refresh_table_view(self.ui.pivot_table)
                    self.refresh_table_view(self.ui.frozen_table)
                    self.make_pivot_headers()
                    return

    def receive_classes_updated(self, db_map_data):
        if not self.pivot_table_model:
            return
        for db_map, items in db_map_data.items():
            for item in items:
                if item["id"] == self.current_class_id.get(db_map):
                    self.do_reload_pivot_table()
                    return

    def receive_classes_removed(self, db_map_data):
        if not self.pivot_table_model:
            return
        for db_map, items in db_map_data.items():
            for item in items:
                if item["id"] == self.current_class_id.get(db_map):
                    self.current_class_type = None
                    self.current_class_id = {}
                    self.clear_pivot_table()
                    return

    def receive_alternatives_added(self, db_map_data):
        """Reacts to alternatives added event."""
        super().receive_alternatives_added(db_map_data)
        self.receive_alternatives_added_or_removed(db_map_data, action="add")

    def receive_scenarios_added(self, db_map_data):
        """Reacts to scenarios added event."""
        super().receive_scenarios_added(db_map_data)
        self.receive_scenarios_added_or_removed(db_map_data, action="add")

    def receive_objects_added(self, db_map_data):
        """Reacts to objects added event."""
        super().receive_objects_added(db_map_data)
        self.receive_objects_added_or_removed(db_map_data, action="add")

    def receive_relationships_added(self, db_map_data):
        """Reacts to relationships added event."""
        super().receive_relationships_added(db_map_data)
        self.receive_relationships_added_or_removed(db_map_data, action="add")

    def receive_parameter_definitions_added(self, db_map_data):
        """Reacts to parameter definitions added event."""
        super().receive_parameter_definitions_added(db_map_data)
        self.receive_parameter_definitions_added_or_removed(db_map_data,
                                                            action="add")

    def receive_parameter_values_added(self, db_map_data):
        """Reacts to parameter values added event."""
        super().receive_parameter_values_added(db_map_data)
        self.receive_parameter_values_added_or_removed(db_map_data,
                                                       action="add")

    def receive_alternatives_updated(self, db_map_data):
        """Reacts to alternatives updated event."""
        super().receive_alternatives_updated(db_map_data)
        if not self.pivot_table_model:
            return
        self.refresh_table_view(self.ui.pivot_table)
        self.refresh_table_view(self.ui.frozen_table)
        self.make_pivot_headers()

    def receive_object_classes_updated(self, db_map_data):
        """Reacts to object classes updated event."""
        super().receive_object_classes_updated(db_map_data)
        self.receive_classes_updated(db_map_data)

    def receive_relationship_classes_updated(self, db_map_data):
        """Reacts to relationship classes updated event."""
        super().receive_relationship_classes_updated(db_map_data)
        self.receive_classes_updated(db_map_data)

    def receive_objects_updated(self, db_map_data):
        """Reacts to objects updated event."""
        super().receive_objects_updated(db_map_data)
        self.receive_db_map_data_updated(db_map_data,
                                         get_class_id=lambda x: x["class_id"])

    def receive_relationships_updated(self, db_map_data):
        """Reacts to relationships updated event."""
        super().receive_relationships_updated(db_map_data)
        self.receive_db_map_data_updated(db_map_data,
                                         get_class_id=lambda x: x["class_id"])

    def receive_parameter_values_updated(self, db_map_data):
        """Reacts to parameter values added event."""
        super().receive_parameter_values_updated(db_map_data)
        self.receive_db_map_data_updated(
            db_map_data,
            get_class_id=lambda x: x.get("object_class_id") or x.get(
                "relationship_class_id"))

    def receive_parameter_definitions_updated(self, db_map_data):
        """Reacts to parameter definitions updated event."""
        super().receive_parameter_definitions_updated(db_map_data)
        self.receive_db_map_data_updated(
            db_map_data,
            get_class_id=lambda x: x.get("object_class_id") or x.get(
                "relationship_class_id"))

    def receive_scenarios_updated(self, db_map_data):
        super().receive_scenarios_updated(db_map_data)
        if not self.pivot_table_model:
            return
        self.pivot_table_model.receive_scenarios_updated(db_map_data)

    def receive_alternatives_removed(self, db_map_data):
        """Reacts to alternatives removed event."""
        super().receive_alternatives_removed(db_map_data)
        self.receive_alternatives_added_or_removed(db_map_data,
                                                   action="remove")

    def receive_scenarios_removed(self, db_map_data):
        """Reacts to scenarios removed event."""
        super().receive_scenarios_removed(db_map_data)
        self.receive_scenarios_added_or_removed(db_map_data, action="remove")

    def receive_object_classes_removed(self, db_map_data):
        """Reacts to object classes removed event."""
        super().receive_object_classes_removed(db_map_data)
        self.receive_classes_removed(db_map_data)

    def receive_relationship_classes_removed(self, db_map_data):
        """Reacts to relationship classes remove event."""
        super().receive_relationship_classes_removed(db_map_data)
        self.receive_classes_removed(db_map_data)

    def receive_objects_removed(self, db_map_data):
        """Reacts to objects removed event."""
        super().receive_objects_removed(db_map_data)
        self.receive_objects_added_or_removed(db_map_data, action="remove")

    def receive_relationships_removed(self, db_map_data):
        """Reacts to relationships removed event."""
        super().receive_relationships_removed(db_map_data)
        self.receive_relationships_added_or_removed(db_map_data,
                                                    action="remove")

    def receive_parameter_definitions_removed(self, db_map_data):
        """Reacts to parameter definitions removed event."""
        super().receive_parameter_definitions_removed(db_map_data)
        self.receive_parameter_definitions_added_or_removed(db_map_data,
                                                            action="remove")

    def receive_parameter_values_removed(self, db_map_data):
        """Reacts to parameter values removed event."""
        super().receive_parameter_values_removed(db_map_data)
        self.receive_parameter_values_added_or_removed(db_map_data,
                                                       action="remove")

    def receive_session_rolled_back(self, db_maps):
        """Reacts to session rolled back event."""
        super().receive_session_rolled_back(db_maps)
        self.reload_pivot_table()
예제 #3
0
class ProfileManager(QObject):
    profile_changed = Signal(Profile)

    def __init__(self, menu, parent=None):
        super().__init__(parent)
        self.menu = menu
        actions = self.menu.actions()
        self.sep = actions[-2]
        self.parent = parent
        self.profiles = []
        self.actGroup = None
        self.active_profile = None
        self.load()
        QApplication.instance().aboutToQuit.connect(self.save)

    def load(self):
        settings = QSettings()
        settings.beginGroup('profiles')
        groups = settings.childGroups()
        self.profiles = [
            Profile(p,
                    settings.value(f'{p}/path'), settings.value(f'{p}/mask'),
                    settings.value(f'{p}/pattern')) for p in groups
        ]
        settings.endGroup()
        self.actGroup = QActionGroup(self.parent)
        self.actGroup.triggered.connect(self.set_active_profile)
        active = settings.value('active_profile')
        self.active_profile = self.get_profile(active)
        if len(self.profiles) > 0:
            for name in self.names():
                action = self.do_add_action(name)
                if name == active:
                    action.setChecked(True)

    def save(self):
        settings = QSettings()
        settings.beginGroup('profiles')
        settings.remove('')
        for p in self.profiles:
            settings.setValue(f'{p.name}/path', p.path)
            settings.setValue(f'{p.name}/mask', p.mask)
            settings.setValue(f'{p.name}/pattern', p.pattern)
        settings.endGroup()
        if self.active_profile is not None:
            settings.setValue('active_profile', self.active_profile.name)

    def names(self):
        for p in self.profiles:
            yield p.name

    def add_action(self, name, path, mask, pattern):
        if name in self.names():
            app = QApplication.instance()
            QMessageBox.warning(
                self.parent, app.applicationName(),
                app.translate('profile_manager',
                              '{} already exists').format(name))
        else:
            self.profiles.append(Profile(name, path, mask, pattern))
            self.do_add_action(name)

    def do_add_action(self, name):
        action = QAction(name, self.menu)
        self.menu.insertAction(self.sep, action)
        action.setCheckable(True)
        self.actGroup.addAction(action)
        return action

    def add_from_dialog(self, dialog):
        self.add_action(dialog.get_name(), dialog.get_path(),
                        dialog.get_mask(), dialog.get_pattern())

    def get_profile(self, name):
        for p in self.profiles:
            if name == p.name:
                return p
        return None

    def set_active_profile(self):
        action = self.actGroup.checkedAction()
        self.active_profile = self.get_profile(action.text()) if action \
            is not None else None
        self.profile_changed.emit(self.active_profile)

    def reset_profiles(self, profiles):
        self.clear_menu()
        self.profiles = profiles
        if len(profiles) > 0:
            if self.active_profile.name not in (p.name for p in profiles):
                active = profiles[0].name
            else:
                active = self.active_profile.name
            for name in self.names():
                action = self.do_add_action(name)
                if name == active:
                    action.setChecked(True)
        else:
            active = None
        self.active_profile = self.get_profile(active)
        self.profile_changed.emit(self.active_profile)

    def clear_menu(self):
        while len(self.actGroup.actions()) > 0:
            self.actGroup.removeAction(self.actGroup.actions()[0])
예제 #4
0
class TabularViewMixin:
    """Provides the pivot table and its frozen table for the DS form."""

    _PARAMETER_VALUE = "Parameter value"
    _INDEX_EXPANSION = "Index expansion"
    _RELATIONSHIP = "Relationship"

    _PARAMETER = "parameter"
    _INDEX = "index"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # current state of ui
        self.current = None  # Current QModelIndex selected in one of the entity tree views
        self.current_class_type = None
        self.current_class_id = None
        self.current_input_type = self._PARAMETER_VALUE
        self.filter_menus = {}
        self.class_pivot_preferences = {}
        self.PivotPreferences = namedtuple(
            "PivotPreferences", ["index", "columns", "frozen", "frozen_value"])
        title_action = TitleWidgetAction("Input type", self)
        self.ui.menuPivot_table.addAction(title_action)
        self.input_type_action_group = QActionGroup(self)
        actions = {
            input_type: self.input_type_action_group.addAction(input_type)
            for input_type in
            [self._PARAMETER_VALUE, self._INDEX_EXPANSION, self._RELATIONSHIP]
        }
        for action in actions.values():
            action.setCheckable(True)
            self.ui.menuPivot_table.addAction(action)
        actions[self.current_input_type].setChecked(True)
        self.pivot_table_proxy = PivotTableSortFilterProxy()
        self.pivot_table_model = None
        self.frozen_table_model = FrozenTableModel(self)
        self.ui.pivot_table.setModel(self.pivot_table_proxy)
        self.ui.pivot_table.connect_data_store_form(self)
        self.ui.frozen_table.setModel(self.frozen_table_model)
        self.ui.frozen_table.verticalHeader().setDefaultSectionSize(
            self.default_row_height)

    def add_menu_actions(self):
        """Adds toggle view actions to View menu."""
        super().add_menu_actions()
        self.ui.menuView.addSeparator()
        self.ui.menuView.addAction(
            self.ui.dockWidget_pivot_table.toggleViewAction())
        self.ui.menuView.addAction(
            self.ui.dockWidget_frozen_table.toggleViewAction())

    def connect_signals(self):
        """Connects signals to slots."""
        super().connect_signals()
        self.ui.treeView_object.selectionModel().currentChanged.connect(
            self._handle_entity_tree_current_changed)
        self.ui.treeView_relationship.selectionModel().currentChanged.connect(
            self._handle_entity_tree_current_changed)
        self.ui.pivot_table.horizontalHeader().header_dropped.connect(
            self.handle_header_dropped)
        self.ui.pivot_table.verticalHeader().header_dropped.connect(
            self.handle_header_dropped)
        self.ui.frozen_table.header_dropped.connect(self.handle_header_dropped)
        self.ui.frozen_table.selectionModel().currentChanged.connect(
            self.change_frozen_value)
        self.input_type_action_group.triggered.connect(
            self.do_reload_pivot_table)
        self.ui.dockWidget_pivot_table.visibilityChanged.connect(
            self._handle_pivot_table_visibility_changed)
        self.ui.dockWidget_frozen_table.visibilityChanged.connect(
            self._handle_frozen_table_visibility_changed)

    def init_models(self):
        """Initializes models."""
        super().init_models()
        self.clear_pivot_table()

    @Slot("QModelIndex", object)
    def _set_model_data(self, index, value):
        self.pivot_table_proxy.setData(index, value)

    @property
    def current_object_class_id_list(self):
        if self.current_class_type == "object class":
            return [self.current_class_id]
        relationship_class = self.db_mngr.get_item(self.db_map,
                                                   "relationship class",
                                                   self.current_class_id)
        return [
            int(id_)
            for id_ in relationship_class["object_class_id_list"].split(",")
        ]

    @property
    def current_object_class_name_list(self):
        if self.current_class_type == "object class":
            return [
                self.db_mngr.get_item(self.db_map, "object class",
                                      self.current_class_id)["name"]
            ]
        relationship_class = self.db_mngr.get_item(self.db_map,
                                                   "relationship class",
                                                   self.current_class_id)
        return fix_name_ambiguity(
            relationship_class["object_class_name_list"].split(","))

    @staticmethod
    def _is_class_index(index):
        """Returns whether or not the given tree index is a class index.

        Args:
            index (QModelIndex): index from object or relationship tree
        Returns:
            bool
        """
        return index.column() == 0 and not index.parent().parent().isValid()

    @Slot(bool)
    def _handle_pivot_table_visibility_changed(self, visible):
        if visible:
            self.reload_pivot_table()
            self.reload_frozen_table()
            self.ui.dockWidget_frozen_table.setVisible(True)

    @Slot(bool)
    def _handle_frozen_table_visibility_changed(self, visible):
        if visible:
            self.ui.dockWidget_pivot_table.show()

    @Slot("QModelIndex", "QModelIndex")
    def _handle_entity_tree_current_changed(self, current, previous):
        if self.ui.dockWidget_pivot_table.isVisible():
            self.reload_pivot_table(current=current)
            self.reload_frozen_table()

    def _get_entities(self, class_id=None, class_type=None):
        """Returns a list of dict items from the object or relationship tree model
        corresponding to the given class id.

        Args:
            class_id (int)
            class_type (str)

        Returns:
            list(dict)
        """
        if class_id is None:
            class_id = self.current_class_id
        if class_type is None:
            class_type = self.current_class_type
        entity_type = {
            "object class": "object",
            "relationship class": "relationship"
        }[class_type]
        return self.db_mngr.get_items_by_field(self.db_map, entity_type,
                                               "class_id", class_id)

    def load_empty_relationship_data(self, objects_per_class=None):
        """Returns a dict containing all possible relationships in the current class.

        Args:
            objects_per_class (dict)

        Returns:
            dict: Key is object id tuple, value is None.
        """
        if objects_per_class is None:
            objects_per_class = dict()
        if self.current_class_type == "object class":
            return {}
        object_id_sets = []
        for obj_cls_id in self.current_object_class_id_list:
            objects = objects_per_class.get(obj_cls_id, None)
            if objects is None:
                objects = self._get_entities(obj_cls_id, "object class")
            id_set = {item["id"]: None for item in objects}
            object_id_sets.append(list(id_set.keys()))
        return dict.fromkeys(product(*object_id_sets))

    def load_full_relationship_data(self, relationships=None, action="add"):
        """Returns a dict of relationships in the current class.

        Returns:
            dict: Key is object id tuple, value is relationship id.
        """
        if self.current_class_type == "object class":
            return {}
        if relationships is None:
            relationships = self._get_entities()
        get_id = {"add": lambda x: x["id"], "remove": lambda x: None}[action]
        return {
            tuple(int(id_)
                  for id_ in x["object_id_list"].split(',')): get_id(x)
            for x in relationships
        }

    def load_relationship_data(self):
        """Returns a dict that merges empty and full relationship data.

        Returns:
            dict: Key is object id tuple, value is True if a relationship exists, False otherwise.
        """
        data = self.load_empty_relationship_data()
        data.update(self.load_full_relationship_data())
        return data

    def _get_parameter_value_or_def_ids(self, item_type):
        """Returns a list of integer ids from the parameter model
        corresponding to the currently selected class and the given item type.

        Args:
            item_type (str): either "parameter value" or "parameter definition"

        Returns:
            list(int)
        """
        class_id_field = {
            "object class": "object_class_id",
            "relationship class": "relationship_class_id"
        }[self.current_class_type]
        return [
            x["id"] for x in self.db_mngr.get_items_by_field(
                self.db_map, item_type, class_id_field, self.current_class_id)
        ]

    def _get_parameter_values_or_defs(self, item_type):
        """Returns a list of dict items from the parameter model
        corresponding to the currently selected class and the given item type.

        Args:
            item_type (str): either "parameter value" or "parameter definition"

        Returns:
            list(dict)
        """
        ids = self._get_parameter_value_or_def_ids(item_type)
        return [
            self.db_mngr.get_item(self.db_map, item_type, id_) for id_ in ids
        ]

    def load_empty_parameter_value_data(self,
                                        entities=None,
                                        parameter_ids=None):
        """Returns a dict containing all possible combinations of entities and parameters for the current class.

        Args:
            entities (list, optional): if given, only load data for these entities
            parameter_ids (set, optional): if given, only load data for these parameter definitions

        Returns:
            dict: Key is a tuple object_id, ..., parameter_id, value is None.
        """
        if entities is None:
            entities = self._get_entities()
        if parameter_ids is None:
            parameter_ids = self._get_parameter_value_or_def_ids(
                "parameter definition")
        if self.current_class_type == "relationship class":
            entity_ids = [
                tuple(int(id_) for id_ in e["object_id_list"].split(','))
                for e in entities
            ]
        else:
            entity_ids = [(e["id"], ) for e in entities]
        if not entity_ids:
            entity_ids = [
                tuple(None for _ in self.current_object_class_id_list)
            ]
        if not parameter_ids:
            parameter_ids = [None]
        return {
            entity_id + (parameter_id, ): None
            for entity_id in entity_ids for parameter_id in parameter_ids
        }

    def load_full_parameter_value_data(self,
                                       parameter_values=None,
                                       action="add"):
        """Returns a dict of parameter values for the current class.

        Args:
            parameter_values (list, optional)
            action (str)

        Returns:
            dict: Key is a tuple object_id, ..., parameter_id, value is the parameter value.
        """
        if parameter_values is None:
            parameter_values = self._get_parameter_values_or_defs(
                "parameter value")
        get_id = {"add": lambda x: x["id"], "remove": lambda x: None}[action]
        if self.current_class_type == "object class":
            return {(x["object_id"], x["parameter_id"]): get_id(x)
                    for x in parameter_values}
        return {
            tuple(int(id_) for id_ in x["object_id_list"].split(',')) +
            (x["parameter_id"], ): get_id(x)
            for x in parameter_values
        }

    def load_parameter_value_data(self):
        """Returns a dict that merges empty and full parameter value data.

        Returns:
            dict: Key is a tuple object_id, ..., parameter_id, value is the parameter value or None if not specified.
        """
        data = self.load_empty_parameter_value_data()
        data.update(self.load_full_parameter_value_data())
        return data

    def load_expanded_parameter_value_data(self):
        """
        Returns all permutations of entities as well as parameter indexes and values for the current class.

        Returns:
            dict: Key is a tuple object_id, ..., index, while value is None.
        """
        data = self.load_parameter_value_data()
        return {
            key[:-1] + (index, key[-1]): id_
            for key, id_ in data.items()
            for index in self.db_mngr.get_value_indexes(
                self.db_map, "parameter value", id_)
        }

    def get_pivot_preferences(self):
        """Returns saved pivot preferences.

        Returns:
            tuple, NoneType: pivot tuple, or None if no preference stored
        """
        selection_key = (self.current_class_id, self.current_class_type,
                         self.current_input_type)
        if selection_key in self.class_pivot_preferences:
            rows = self.class_pivot_preferences[selection_key].index
            columns = self.class_pivot_preferences[selection_key].columns
            frozen = self.class_pivot_preferences[selection_key].frozen
            frozen_value = self.class_pivot_preferences[
                selection_key].frozen_value
            return (rows, columns, frozen, frozen_value)
        return None

    @Slot(str)
    def reload_pivot_table(self, current=None):
        """Updates current class (type and id) and reloads pivot table for it."""
        if current is not None:
            self.current = current
        if self.current is None:
            return
        if self._is_class_index(self.current):
            item = self.current.model().item_from_index(self.current)
            class_id = item.db_map_id(self.db_map)
            if self.current_class_id == class_id:
                return
            self.current_class_type = item.item_type
            self.current_class_id = class_id
            self.do_reload_pivot_table()

    @busy_effect
    @Slot("QAction")
    def do_reload_pivot_table(self, action=None):
        """Reloads pivot table.
        """
        if self.current_class_id is None:
            return
        qApp.processEvents()  # pylint: disable=undefined-variable
        if action is None:
            action = self.input_type_action_group.checkedAction()
        self.current_input_type = action.text()
        self.pivot_table_model = {
            self._PARAMETER_VALUE: ParameterValuePivotTableModel,
            self._RELATIONSHIP: RelationshipPivotTableModel,
            self._INDEX_EXPANSION: IndexExpansionPivotTableModel,
        }[self.current_input_type](self)
        self.pivot_table_proxy.setSourceModel(self.pivot_table_model)
        delegate = {
            self._PARAMETER_VALUE: ParameterPivotTableDelegate,
            self._RELATIONSHIP: RelationshipPivotTableDelegate,
            self._INDEX_EXPANSION: ParameterPivotTableDelegate,
        }[self.current_input_type](self)
        self.ui.pivot_table.setItemDelegate(delegate)
        self.pivot_table_model.modelReset.connect(self.make_pivot_headers)
        if self.current_input_type == self._RELATIONSHIP and self.current_class_type != "relationship class":
            self.clear_pivot_table()
            return
        pivot = self.get_pivot_preferences()
        self.wipe_out_filter_menus()
        object_class_ids = dict(
            zip(self.current_object_class_name_list,
                self.current_object_class_id_list))
        self.pivot_table_model.call_reset_model(object_class_ids, pivot)
        self.pivot_table_proxy.clear_filter()

    def clear_pivot_table(self):
        self.wipe_out_filter_menus()
        if self.pivot_table_model:
            self.pivot_table_model.clear_model()
            self.pivot_table_proxy.clear_filter()

    def wipe_out_filter_menus(self):
        while self.filter_menus:
            _, menu = self.filter_menus.popitem()
            menu.wipe_out()

    @Slot()
    def make_pivot_headers(self):
        """
        Turns top left indexes in the pivot table into TabularViewHeaderWidget.
        """
        top_indexes, left_indexes = self.pivot_table_model.top_left_indexes()
        for index in left_indexes:
            proxy_index = self.pivot_table_proxy.mapFromSource(index)
            widget = self.create_header_widget(
                proxy_index.data(Qt.DisplayRole), "columns")
            self.ui.pivot_table.setIndexWidget(proxy_index, widget)
        for index in top_indexes:
            proxy_index = self.pivot_table_proxy.mapFromSource(index)
            widget = self.create_header_widget(
                proxy_index.data(Qt.DisplayRole), "rows")
            self.ui.pivot_table.setIndexWidget(proxy_index, widget)
        QTimer.singleShot(0, self._resize_pivot_header_columns)

    @Slot()
    def _resize_pivot_header_columns(self):
        top_indexes, _ = self.pivot_table_model.top_left_indexes()
        for index in top_indexes:
            self.ui.pivot_table.resizeColumnToContents(index.column())

    def make_frozen_headers(self):
        """
        Turns indexes in the first row of the frozen table into TabularViewHeaderWidget.
        """
        for column in range(self.frozen_table_model.columnCount()):
            index = self.frozen_table_model.index(0, column)
            widget = self.create_header_widget(index.data(Qt.DisplayRole),
                                               "frozen",
                                               with_menu=False)
            self.ui.frozen_table.setIndexWidget(index, widget)
            self.ui.frozen_table.horizontalHeader().resizeSection(
                column,
                widget.size().width())

    def create_filter_menu(self, identifier):
        """Returns a filter menu for given given object class identifier.

        Args:
            identifier (int)

        Returns:
            TabularViewFilterMenu
        """
        _get_field = lambda *args: self.db_mngr.get_field(self.db_map, *args)
        if identifier not in self.filter_menus:
            pivot_top_left_header = self.pivot_table_model.top_left_headers[
                identifier]
            data_to_value = pivot_top_left_header.header_data
            self.filter_menus[identifier] = menu = TabularViewFilterMenu(
                self, identifier, data_to_value, show_empty=False)
            index_values = dict.fromkeys(
                self.pivot_table_model.model.index_values.get(identifier, []))
            index_values.pop(None, None)
            menu.set_filter_list(index_values.keys())
            menu.filterChanged.connect(self.change_filter)
        return self.filter_menus[identifier]

    def create_header_widget(self, identifier, area, with_menu=True):
        """
        Returns a TabularViewHeaderWidget for given object class identifier.

        Args:
            identifier (str)
            area (str)
            with_menu (bool)

        Returns:
            TabularViewHeaderWidget
        """
        menu = self.create_filter_menu(identifier) if with_menu else None
        widget = TabularViewHeaderWidget(identifier,
                                         area,
                                         menu=menu,
                                         parent=self)
        widget.header_dropped.connect(self.handle_header_dropped)
        return widget

    @staticmethod
    def _get_insert_index(pivot_list, catcher, position):
        """Returns an index for inserting a new element in the given pivot list.

        Returns:
            int
        """
        if isinstance(catcher, TabularViewHeaderWidget):
            i = pivot_list.index(catcher.identifier)
            if position == "after":
                i += 1
        else:
            i = 0
        return i

    @Slot(object, object, str)
    def handle_header_dropped(self, dropped, catcher, position=""):
        """
        Updates pivots when a header is dropped.

        Args:
            dropped (TabularViewHeaderWidget)
            catcher (TabularViewHeaderWidget, PivotTableHeaderView, FrozenTableView)
            position (str): either "before", "after", or ""
        """
        top_indexes, left_indexes = self.pivot_table_model.top_left_indexes()
        rows = [index.data(Qt.DisplayRole) for index in top_indexes]
        columns = [index.data(Qt.DisplayRole) for index in left_indexes]
        frozen = self.frozen_table_model.headers
        dropped_list = {
            "columns": columns,
            "rows": rows,
            "frozen": frozen
        }[dropped.area]
        catcher_list = {
            "columns": columns,
            "rows": rows,
            "frozen": frozen
        }[catcher.area]
        dropped_list.remove(dropped.identifier)
        i = self._get_insert_index(catcher_list, catcher, position)
        catcher_list.insert(i, dropped.identifier)
        if dropped.area == "frozen" or catcher.area == "frozen":
            if frozen:
                frozen_values = self.find_frozen_values(frozen)
                self.frozen_table_model.reset_model(frozen_values, frozen)
                self.make_frozen_headers()
                self.ui.frozen_table.resizeColumnsToContents()
            else:
                self.frozen_table_model.clear_model()
        frozen_value = self.get_frozen_value(
            self.ui.frozen_table.currentIndex())
        self.pivot_table_model.set_pivot(rows, columns, frozen, frozen_value)
        # save current pivot
        self.class_pivot_preferences[(
            self.current_class_id, self.current_class_type,
            self.current_input_type)] = self.PivotPreferences(
                rows, columns, frozen, frozen_value)
        self.make_pivot_headers()

    def get_frozen_value(self, index):
        """
        Returns the value in the frozen table corresponding to the given index.

        Args:
            index (QModelIndex)
        Returns:
            tuple
        """
        if not index.isValid():
            return tuple(None
                         for _ in range(self.frozen_table_model.columnCount()))
        return self.frozen_table_model.row(index)

    @Slot("QModelIndex", "QModelIndex")
    def change_frozen_value(self, current, previous):
        """Sets the frozen value from selection in frozen table.
        """
        frozen_value = self.get_frozen_value(current)
        self.pivot_table_model.set_frozen_value(frozen_value)
        # store pivot preferences
        self.class_pivot_preferences[(
            self.current_class_id, self.current_class_type,
            self.current_input_type)] = self.PivotPreferences(
                self.pivot_table_model.model.pivot_rows,
                self.pivot_table_model.model.pivot_columns,
                self.pivot_table_model.model.pivot_frozen,
                self.pivot_table_model.model.frozen_value,
            )

    @Slot(str, set, bool)
    def change_filter(self, identifier, valid_values, has_filter):
        if has_filter:
            self.pivot_table_proxy.set_filter(identifier, valid_values)
        else:
            self.pivot_table_proxy.set_filter(
                identifier, None)  # None means everything passes

    def reload_frozen_table(self):
        """Resets the frozen model according to new selection in entity trees."""
        if not self.pivot_table_model:
            return
        frozen = self.pivot_table_model.model.pivot_frozen
        frozen_value = self.pivot_table_model.model.frozen_value
        frozen_values = self.find_frozen_values(frozen)
        self.frozen_table_model.reset_model(frozen_values, frozen)
        self.make_frozen_headers()
        if frozen_value in frozen_values:
            # update selected row
            ind = frozen_values.index(frozen_value)
            self.ui.frozen_table.selectionModel().blockSignals(
                True)  # prevent selectionChanged signal when updating
            self.ui.frozen_table.selectRow(ind)
            self.ui.frozen_table.selectionModel().blockSignals(False)
        else:
            # frozen value not found, remove selection
            self.ui.frozen_table.selectionModel().blockSignals(
                True)  # prevent selectionChanged signal when updating
            self.ui.frozen_table.clearSelection()
            self.ui.frozen_table.selectionModel().blockSignals(False)
        self.ui.frozen_table.resizeColumnsToContents()

    def find_frozen_values(self, frozen):
        """Returns a list of tuples containing unique values (object ids) for the frozen indexes (object class ids).

        Args:
            frozen (tuple(int)): A tuple of currently frozen indexes
        Returns:
            list(tuple(list(int)))
        """
        return list(
            dict.fromkeys(
                zip(*[
                    self.pivot_table_model.model.index_values.get(k, [])
                    for k in frozen
                ])).keys())

    # FIXME: Move this to the models
    @staticmethod
    def refresh_table_view(table_view):
        top_left = table_view.indexAt(table_view.rect().topLeft())
        bottom_right = table_view.indexAt(table_view.rect().bottomRight())
        if not bottom_right.isValid():
            model = table_view.model()
            bottom_right = table_view.model().index(model.rowCount() - 1,
                                                    model.columnCount() - 1)
        table_view.model().dataChanged.emit(top_left, bottom_right)

    @Slot(str)
    def update_filter_menus(self, action):
        for identifier, menu in self.filter_menus.items():
            index_values = dict.fromkeys(
                self.pivot_table_model.model.index_values.get(identifier, []))
            index_values.pop(None, None)
            if action == "add":
                menu.add_items_to_filter_list(list(index_values.keys()))
            elif action == "remove":
                previous = menu._filter._filter_model._data_set
                menu.remove_items_from_filter_list(
                    list(previous - index_values.keys()))
        self.reload_frozen_table()

    def receive_objects_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        items = db_map_data.get(self.db_map, set())
        if self.pivot_table_model.receive_objects_added_or_removed(
                items, action):
            self.update_filter_menus(action)

    def receive_relationships_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        if self.current_class_type != "relationship class":
            return
        items = db_map_data.get(self.db_map, set())
        relationships = [
            x for x in items if x["class_id"] == self.current_class_id
        ]
        if not relationships:
            return
        if self.pivot_table_model.receive_relationships_added_or_removed(
                relationships, action):
            self.update_filter_menus(action)

    def receive_parameter_definitions_added_or_removed(self, db_map_data,
                                                       action):
        if not self.pivot_table_model:
            return
        items = db_map_data.get(self.db_map, set())
        parameters = [
            x for x in items
            if (x.get("object_class_id") or x.get("relationship_class_id")
                ) == self.current_class_id
        ]
        if not parameters:
            return
        if self.pivot_table_model.receive_parameter_definitions_added_or_removed(
                parameters, action):
            self.update_filter_menus(action)

    def receive_parameter_values_added_or_removed(self, db_map_data, action):
        if not self.pivot_table_model:
            return
        items = db_map_data.get(self.db_map, set())
        parameter_values = [
            x for x in items
            if (x.get("object_class_id") or x.get("relationship_class_id")
                ) == self.current_class_id
        ]
        if not parameter_values:
            return
        if self.pivot_table_model.receive_parameter_values_added_or_removed(
                parameter_values, action):
            self.update_filter_menus(action)

    def receive_db_map_data_updated(self, db_map_data, get_class_id):
        if not self.pivot_table_model:
            return
        items = db_map_data.get(self.db_map, set())
        for item in items:
            if get_class_id(item) == self.current_class_id:
                self.refresh_table_view(self.ui.pivot_table)
                self.refresh_table_view(self.ui.frozen_table)
                self.make_pivot_headers()
                break

    def receive_classes_removed(self, db_map_data):
        if not self.pivot_table_model:
            return
        items = db_map_data.get(self.db_map, set())
        for item in items:
            if item["id"] == self.current_class_id:
                self.current_class_type = None
                self.current_class_id = None
                self.clear_pivot_table()
                break

    def receive_objects_added(self, db_map_data):
        """Reacts to objects added event."""
        super().receive_objects_added(db_map_data)
        self.receive_objects_added_or_removed(db_map_data, action="add")

    def receive_relationships_added(self, db_map_data):
        """Reacts to relationships added event."""
        super().receive_relationships_added(db_map_data)
        self.receive_relationships_added_or_removed(db_map_data, action="add")

    def receive_parameter_definitions_added(self, db_map_data):
        """Reacts to parameter definitions added event."""
        super().receive_parameter_definitions_added(db_map_data)
        self.receive_parameter_definitions_added_or_removed(db_map_data,
                                                            action="add")

    def receive_parameter_values_added(self, db_map_data):
        """Reacts to parameter values added event."""
        super().receive_parameter_values_added(db_map_data)
        self.receive_parameter_values_added_or_removed(db_map_data,
                                                       action="add")

    def receive_object_classes_updated(self, db_map_data):
        """Reacts to object classes updated event."""
        super().receive_object_classes_updated(db_map_data)
        self.receive_db_map_data_updated(db_map_data,
                                         get_class_id=lambda x: x["id"])

    def receive_objects_updated(self, db_map_data):
        """Reacts to objects updated event."""
        super().receive_objects_updated(db_map_data)
        self.receive_db_map_data_updated(db_map_data,
                                         get_class_id=lambda x: x["class_id"])

    def receive_relationship_classes_updated(self, db_map_data):
        """Reacts to relationship classes updated event."""
        super().receive_relationship_classes_updated(db_map_data)
        self.receive_db_map_data_updated(db_map_data,
                                         get_class_id=lambda x: x["id"])

    def receive_relationships_updated(self, db_map_data):
        """Reacts to relationships updated event."""
        super().receive_relationships_updated(db_map_data)
        self.receive_db_map_data_updated(db_map_data,
                                         get_class_id=lambda x: x["class_id"])

    def receive_parameter_values_updated(self, db_map_data):
        """Reacts to parameter values added event."""
        super().receive_parameter_values_updated(db_map_data)
        self.receive_db_map_data_updated(
            db_map_data,
            get_class_id=lambda x: x.get("object_class_id") or x.get(
                "relationship_class_id"))

    def receive_parameter_definitions_updated(self, db_map_data):
        """Reacts to parameter definitions updated event."""
        super().receive_parameter_definitions_updated(db_map_data)
        self.receive_db_map_data_updated(
            db_map_data,
            get_class_id=lambda x: x.get("object_class_id") or x.get(
                "relationship_class_id"))

    def receive_object_classes_removed(self, db_map_data):
        """Reacts to object classes removed event."""
        super().receive_object_classes_removed(db_map_data)
        self.receive_classes_removed(db_map_data)

    def receive_objects_removed(self, db_map_data):
        """Reacts to objects removed event."""
        super().receive_objects_removed(db_map_data)
        self.receive_objects_added_or_removed(db_map_data, action="remove")

    def receive_relationship_classes_removed(self, db_map_data):
        """Reacts to relationship classes remove event."""
        super().receive_relationship_classes_removed(db_map_data)
        self.receive_classes_removed(db_map_data)

    def receive_relationships_removed(self, db_map_data):
        """Reacts to relationships removed event."""
        super().receive_relationships_removed(db_map_data)
        self.receive_relationships_added_or_removed(db_map_data,
                                                    action="remove")

    def receive_parameter_definitions_removed(self, db_map_data):
        """Reacts to parameter definitions removed event."""
        super().receive_parameter_definitions_removed(db_map_data)
        self.receive_parameter_definitions_added_or_removed(db_map_data,
                                                            action="remove")

    def receive_parameter_values_removed(self, db_map_data):
        """Reacts to parameter values removed event."""
        super().receive_parameter_values_removed(db_map_data)
        self.receive_parameter_values_added_or_removed(db_map_data,
                                                       action="remove")

    def receive_session_rolled_back(self, db_maps):
        """Reacts to session rolled back event."""
        super().receive_session_rolled_back(db_maps)
        self.reload_pivot_table()
        self.reload_frozen_table()
예제 #5
0
class MainWindow(QMainWindow, Ui_MainWindow):
    """docstring for MainWindow."""
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self._csvFilePath = ""
        self.serialport = serial.Serial()
        self.receiver_thread = readerThread(self)
        self.receiver_thread.setPort(self.serialport)
        self._localEcho = None

        self.setupUi(self)
        self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
        self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        font = QtGui.QFont()
        font.setFamily(EDITOR_FONT)
        font.setPointSize(10)
        self.txtEdtOutput.setFont(font)
        self.txtEdtInput.setFont(font)
        self.quickSendTable.setFont(font)
        if UI_FONT is not None:
            font = QtGui.QFont()
            font.setFamily(UI_FONT)
            font.setPointSize(9)
            self.dockWidget_PortConfig.setFont(font)
            self.dockWidget_SendHex.setFont(font)
            self.dockWidget_QuickSend.setFont(font)
        self.setupFlatUi()
        self.onEnumPorts()

        icon = QtGui.QIcon(":/icon.ico")
        self.setWindowIcon(icon)
        self.actionAbout.setIcon(icon)

        icon = QtGui.QIcon(":/qt_logo_16.ico")
        self.actionAbout_Qt.setIcon(icon)

        self._viewGroup = QActionGroup(self)
        self._viewGroup.addAction(self.actionAscii)
        self._viewGroup.addAction(self.actionHex_lowercase)
        self._viewGroup.addAction(self.actionHEX_UPPERCASE)
        self._viewGroup.setExclusive(True)

        # bind events
        self.actionOpen_Cmd_File.triggered.connect(self.openCSV)
        self.actionSave_Log.triggered.connect(self.onSaveLog)
        self.actionExit.triggered.connect(self.onExit)

        self.actionOpen.triggered.connect(self.openPort)
        self.actionClose.triggered.connect(self.closePort)

        self.actionPort_Config_Panel.triggered.connect(self.onTogglePrtCfgPnl)
        self.actionQuick_Send_Panel.triggered.connect(self.onToggleQckSndPnl)
        self.actionSend_Hex_Panel.triggered.connect(self.onToggleHexPnl)
        self.dockWidget_PortConfig.visibilityChanged.connect(self.onVisiblePrtCfgPnl)
        self.dockWidget_QuickSend.visibilityChanged.connect(self.onVisibleQckSndPnl)
        self.dockWidget_SendHex.visibilityChanged.connect(self.onVisibleHexPnl)
        self.actionLocal_Echo.triggered.connect(self.onLocalEcho)
        self.actionAlways_On_Top.triggered.connect(self.onAlwaysOnTop)

        self.actionAscii.triggered.connect(self.onViewChanged)
        self.actionHex_lowercase.triggered.connect(self.onViewChanged)
        self.actionHEX_UPPERCASE.triggered.connect(self.onViewChanged)

        self.actionAbout.triggered.connect(self.onAbout)
        self.actionAbout_Qt.triggered.connect(self.onAboutQt)

        self.btnOpen.clicked.connect(self.onOpen)
        self.btnClear.clicked.connect(self.onClear)
        self.btnSaveLog.clicked.connect(self.onSaveLog)
        self.btnEnumPorts.clicked.connect(self.onEnumPorts)
        self.btnSendHex.clicked.connect(self.sendHex)

        self.receiver_thread.read.connect(self.receive)
        self.receiver_thread.exception.connect(self.readerExcept)
        self._signalMap = QSignalMapper(self)
        self._signalMap.mapped[int].connect(self.tableClick)

        # initial action
        self.actionHEX_UPPERCASE.setChecked(True)
        self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE)
        self.initQuickSend()
        self.restoreLayout()
        self.moveScreenCenter()
        self.syncMenu()
        
        if self.isMaximized():
            self.setMaximizeButton("restore")
        else:
            self.setMaximizeButton("maximize")
            
        self.LoadSettings()

    def setupFlatUi(self):
        self._dragPos = self.pos()
        self._isDragging = False
        self.setMouseTracking(True)
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setStyleSheet("""
            QWidget {
                background-color:#99d9ea;
                /*background-image: url(:/background.png);*/
                outline: 1px solid #0057ff;
            }
            QLabel {
                color:#202020;
                font-size:13px;
                font-family:Century;
            }
            
            QComboBox {
                color:#202020;
                font-size:13px;
                font-family:Century Schoolbook;
            }
            QComboBox {
                border: none;
                padding: 1px 18px 1px 3px;
            }
            QComboBox:editable {
                background: white;
            }
            QComboBox:!editable, QComboBox::drop-down:editable {
                background: #62c7e0;
            }
            QComboBox:!editable:hover, QComboBox::drop-down:editable:hover {
                background: #c7eaf3;
            }
            QComboBox:!editable:pressed, QComboBox::drop-down:editable:pressed {
                background: #35b6d7;
            }
            QComboBox:on {
                padding-top: 3px;
                padding-left: 4px;
            }
            QComboBox::drop-down {
                subcontrol-origin: padding;
                subcontrol-position: top right;
                width: 16px;
                border: none;
            }
            QComboBox::down-arrow {
                image: url(:/downarrow.png);
            }
            QComboBox::down-arrow:on {
                image: url(:/uparrow.png);
            }
            
            QGroupBox {
                color:#202020;
                font-size:12px;
                font-family:Century Schoolbook;
                border: 1px solid gray;
                margin-top: 15px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top left;
                left:5px;
                top:3px;
            }
            
            QCheckBox {
                color:#202020;
                spacing: 5px;
                font-size:12px;
                font-family:Century Schoolbook;
            }

            QScrollBar:horizontal {
                background-color:#99d9ea;
                border: none;
                height: 15px;
                margin: 0px 20px 0 20px;
            }
            QScrollBar::handle:horizontal {
                background: #61b9e1;
                min-width: 20px;
            }
            QScrollBar::add-line:horizontal {
                image: url(:/rightarrow.png);
                border: none;
                background: #7ecfe4;
                width: 20px;
                subcontrol-position: right;
                subcontrol-origin: margin;
            }
            QScrollBar::sub-line:horizontal {
                image: url(:/leftarrow.png);
                border: none;
                background: #7ecfe4;
                width: 20px;
                subcontrol-position: left;
                subcontrol-origin: margin;
            }
            
            QScrollBar:vertical {
                background-color:#99d9ea;
                border: none;
                width: 15px;
                margin: 20px 0px 20px 0px;
            }
            QScrollBar::handle::vertical {
                background: #61b9e1;
                min-height: 20px;
            }
            QScrollBar::add-line::vertical {
                image: url(:/downarrow.png);
                border: none;
                background: #7ecfe4;
                height: 20px;
                subcontrol-position: bottom;
                subcontrol-origin: margin;
            }
            QScrollBar::sub-line::vertical {
                image: url(:/uparrow.png);
                border: none;
                background: #7ecfe4;
                height: 20px;
                subcontrol-position: top;
                subcontrol-origin: margin;
            }
            
            QTableView {
                background-color: white;
                /*selection-background-color: #FF92BB;*/
                border: 1px solid #eeeeee;
                color: #2f2f2f;
            }
            QTableView::focus {
                /*border: 1px solid #2a7fff;*/
            }
            QTableView QTableCornerButton::section {
                border: none;
                border-right: 1px solid #eeeeee;
                border-bottom: 1px solid #eeeeee;
                background-color: #8ae6d2;
            }
            QTableView QWidget {
                background-color: white;
            }
            QTableView::item:focus {
                border: 1px red;
                background-color: transparent;
                color: #2f2f2f;
            }
            QHeaderView::section {
                border: none;
                border-right: 1px solid #eeeeee;
                border-bottom: 1px solid #eeeeee;
                padding-left: 2px;
                padding-right: 2px;
                color: #444444;
                background-color: #8ae6d2;
            }
            QTextEdit {
                background-color:white;
                color:#2f2f2f;
                border: 1px solid white;
            }
            QTextEdit::focus {
                border: 1px solid #2a7fff;
            }
            
            QPushButton {
                background-color:#30a7b8;
                border:none;
                color:#ffffff;
                font-size:14px;
                font-family:Century Schoolbook;
            }
            QPushButton:hover {
                background-color:#51c0d1;
            }
            QPushButton:pressed {
                background-color:#3a9ecc;
            }
            
            QMenuBar {
                color: #2f2f2f;
            }
            QMenuBar::item {
                background-color: transparent;
                margin: 8px 0px 0px 0px;
                padding: 1px 8px 1px 8px;
                height: 15px;
            }
            QMenuBar::item:selected {
                background: #51c0d1;
            }
            QMenuBar::item:pressed {
                
            }
            QMenu {
                color: #2f2f2f;
            }
            QMenu {
                margin: 2px;
            }
            QMenu::item {
                padding: 2px 25px 2px 21px;
                border: 1px solid transparent;
            }
            QMenu::item:selected {
                background: #51c0d1;
            }
            QMenu::icon {
                background: transparent;
                border: 2px inset transparent;
            }

            QDockWidget {
                font-size:13px;
                font-family:Century;
                color: #202020;
                titlebar-close-icon: none;
                titlebar-normal-icon: none;
            }
            QDockWidget::title {
                margin: 0;
                padding: 2px;
                subcontrol-origin: content;
                subcontrol-position: right top;
                text-align: left;
                background: #67baed;
                
            }
            QDockWidget::float-button {
                max-width: 12px;
                max-height: 12px;
                background-color:transparent;
                border:none;
                image: url(:/restore_inactive.png);
            }
            QDockWidget::float-button:hover {
                background-color:#227582;
                image: url(:/restore_active.png);
            }
            QDockWidget::float-button:pressed {
                padding: 0;
                background-color:#14464e;
                image: url(:/restore_active.png);
            }
            QDockWidget::close-button {
                max-width: 12px;
                max-height: 12px;
                background-color:transparent;
                border:none;
                image: url(:/close_inactive.png);
            }
            QDockWidget::close-button:hover {
                background-color:#ea5e00;
                image: url(:/close_active.png);
            }
            QDockWidget::close-button:pressed {
                background-color:#994005;
                image: url(:/close_active.png);
                padding: 0;
            }
            
        """)
        self.dockWidgetContents.setStyleSheet("""
            QPushButton {
                min-height:23px;
            }
        """)
        self.dockWidget_QuickSend.setStyleSheet("""
            QPushButton {
                background-color:#27b798;
                font-family:Consolas;
                font-size:12px;
                min-width:46px;
            }
            QPushButton:hover {
                background-color:#3bd5b4;
            }
            QPushButton:pressed {
                background-color:#1d8770;
            }
        """)
        self.dockWidgetContents_2.setStyleSheet("""
            QPushButton {
                min-height:23px;
                min-width:50px;
            }
        """)

        w = self.frameGeometry().width()
        self._minBtn = QPushButton(self)
        self._minBtn.setGeometry(w-103,0,28,24)
        self._minBtn.clicked.connect(self.onMinimize)
        self._minBtn.setStyleSheet("""
            QPushButton {
                background-color:transparent;
                border:none;
                outline: none;
                image: url(:/minimize_inactive.png);
            }
            QPushButton:hover {
                background-color:#227582;
                image: url(:/minimize_active.png);
            }
            QPushButton:pressed {
                background-color:#14464e;
                image: url(:/minimize_active.png);
            }
        """)
        
        self._maxBtn = QPushButton(self)
        self._maxBtn.setGeometry(w-74,0,28,24)
        self._maxBtn.clicked.connect(self.onMaximize)
        self.setMaximizeButton("maximize")
        
        self._closeBtn = QPushButton(self)
        self._closeBtn.setGeometry(w-45,0,36,24)
        self._closeBtn.clicked.connect(self.onExit)
        self._closeBtn.setStyleSheet("""
            QPushButton {
                background-color:transparent;
                border:none;
                outline: none;
                image: url(:/close_inactive.png);
            }
            QPushButton:hover {
                background-color:#ea5e00;
                image: url(:/close_active.png);
            }
            QPushButton:pressed {
                background-color:#994005;
                image: url(:/close_active.png);
            }
        """)

    def resizeEvent(self, event):
        w = event.size().width()
        self._minBtn.move(w-103,0)
        self._maxBtn.move(w-74,0)
        self._closeBtn.move(w-45,0)

    def onMinimize(self):
        self.showMinimized()
    
    def isMaximized(self):
        return ((self.windowState() == Qt.WindowMaximized))
    
    def onMaximize(self):
        if self.isMaximized():
            self.showNormal()
            self.setMaximizeButton("maximize")
        else:
            self.showMaximized()
            self.setMaximizeButton("restore")
    
    def setMaximizeButton(self, style):
        if "maximize" == style:
            self._maxBtn.setStyleSheet("""
                QPushButton {
                    background-color:transparent;
                    border:none;
                    outline: none;
                    image: url(:/maximize_inactive.png);
                }
                QPushButton:hover {
                    background-color:#227582;
                    image: url(:/maximize_active.png);
                }
                QPushButton:pressed {
                    background-color:#14464e;
                    image: url(:/maximize_active.png);
                }
            """)
        elif "restore" == style:
            self._maxBtn.setStyleSheet("""
                QPushButton {
                    background-color:transparent;
                    border:none;
                    outline: none;
                    image: url(:/restore_inactive.png);
                }
                QPushButton:hover {
                    background-color:#227582;
                    image: url(:/restore_active.png);
                }
                QPushButton:pressed {
                    background-color:#14464e;
                    image: url(:/restore_active.png);
                }
            """)
    
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._isDragging = True
            self._dragPos = event.globalPos() - self.pos()
        event.accept()
        
    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.LeftButton and self._isDragging and not self.isMaximized():
            self.move(event.globalPos() - self._dragPos)
        event.accept()

    def mouseReleaseEvent(self, event):
        self._isDragging = False
        event.accept()

    def SaveSettings(self):
        root = ET.Element("MyTerm")
        GUISettings = ET.SubElement(root, "GUISettings")

        PortCfg = ET.SubElement(GUISettings, "PortConfig")
        ET.SubElement(PortCfg, "port").text = self.cmbPort.currentText()
        ET.SubElement(PortCfg, "baudrate").text = self.cmbBaudRate.currentText()
        ET.SubElement(PortCfg, "databits").text = self.cmbDataBits.currentText()
        ET.SubElement(PortCfg, "parity").text = self.cmbParity.currentText()
        ET.SubElement(PortCfg, "stopbits").text = self.cmbStopBits.currentText()
        ET.SubElement(PortCfg, "rtscts").text = self.chkRTSCTS.isChecked() and "on" or "off"
        ET.SubElement(PortCfg, "xonxoff").text = self.chkXonXoff.isChecked() and "on" or "off"

        View = ET.SubElement(GUISettings, "View")
        ET.SubElement(View, "LocalEcho").text = self.actionLocal_Echo.isChecked() and "on" or "off"
        ET.SubElement(View, "ReceiveView").text = self._viewGroup.checkedAction().text()

        with open(get_config_path('settings.xml'), 'w') as f:
            f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(root, encoding='utf-8', pretty_print=True).decode("utf-8"))

    def LoadSettings(self):
        if os.path.isfile(get_config_path("settings.xml")):
            with open(get_config_path("settings.xml"), 'r') as f:
                tree = safeET.parse(f)

            port = tree.findtext('GUISettings/PortConfig/port', default='')
            if port != '':
                self.cmbPort.setCurrentText(port)

            baudrate = tree.findtext('GUISettings/PortConfig/baudrate', default='38400')
            if baudrate != '':
                self.cmbBaudRate.setCurrentText(baudrate)

            databits = tree.findtext('GUISettings/PortConfig/databits', default='8')
            id = self.cmbDataBits.findText(databits)
            if id >= 0:
                self.cmbDataBits.setCurrentIndex(id)

            parity = tree.findtext('GUISettings/PortConfig/parity', default='None')
            id = self.cmbParity.findText(parity)
            if id >= 0:
                self.cmbParity.setCurrentIndex(id)

            stopbits = tree.findtext('GUISettings/PortConfig/stopbits', default='1')
            id = self.cmbStopBits.findText(stopbits)
            if id >= 0:
                self.cmbStopBits.setCurrentIndex(id)

            rtscts = tree.findtext('GUISettings/PortConfig/rtscts', default='off')
            if 'on' == rtscts:
                self.chkRTSCTS.setChecked(True)
            else:
                self.chkRTSCTS.setChecked(False)

            xonxoff = tree.findtext('GUISettings/PortConfig/xonxoff', default='off')
            if 'on' == xonxoff:
                self.chkXonXoff.setChecked(True)
            else:
                self.chkXonXoff.setChecked(False)

            LocalEcho = tree.findtext('GUISettings/View/LocalEcho', default='off')
            if 'on' == LocalEcho:
                self.actionLocal_Echo.setChecked(True)
                self._localEcho = True
            else:
                self.actionLocal_Echo.setChecked(False)
                self._localEcho = False

            ReceiveView = tree.findtext('GUISettings/View/ReceiveView', default='HEX(UPPERCASE)')
            if 'Ascii' in ReceiveView:
                self.actionAscii.setChecked(True)
            elif 'lowercase' in ReceiveView:
                self.actionHex_lowercase.setChecked(True)
            elif 'UPPERCASE' in ReceiveView:
                self.actionHEX_UPPERCASE.setChecked(True)

    def closeEvent(self, event):
        self.saveLayout()
        self.saveCSV()
        self.SaveSettings()
        event.accept()

    def tableClick(self, row):
        self.sendTableRow(row)

    def initQuickSend(self):
        #self.quickSendTable.horizontalHeader().setDefaultSectionSize(40)
        #self.quickSendTable.horizontalHeader().setMinimumSectionSize(25)
        self.quickSendTable.setRowCount(50)
        self.quickSendTable.setColumnCount(20)

        for row in range(50):
            item = QPushButton(str("Send"))
            item.clicked.connect(self._signalMap.map)
            self._signalMap.setMapping(item, row)
            self.quickSendTable.setCellWidget(row, 0, item)
            self.quickSendTable.setRowHeight(row, 20)

        if os.path.isfile(get_config_path('QckSndBckup.csv')):
            self.loadCSV(get_config_path('QckSndBckup.csv'))

        self.quickSendTable.resizeColumnsToContents()

    def openCSV(self):
        fileName = QFileDialog.getOpenFileName(self, "Select a file",
            os.getcwd(), "CSV Files (*.csv)")[0]
        if fileName:
            self.loadCSV(fileName, notifyExcept = True)

    def saveCSV(self):
        # scan table
        rows = self.quickSendTable.rowCount()
        cols = self.quickSendTable.columnCount()

        tmp_data = [[self.quickSendTable.item(row, col) is not None
                    and self.quickSendTable.item(row, col).text() or ''
                    for col in range(1, cols)] for row in range(rows)]

        data = []
        # delete trailing blanks
        for row in tmp_data:
            for idx, d in enumerate(row[::-1]):
                if '' != d:
                    break
            new_row = row[:len(row) - idx]
            data.append(new_row)

        #import pprint
        #pprint.pprint(data, width=120, compact=True)

        # write to file
        with open(get_config_path('QckSndBckup.csv'), 'w') as csvfile:
            csvwriter = csv.writer(csvfile, delimiter=',', lineterminator='\n')
            csvwriter.writerows(data)

    def loadCSV(self, path, notifyExcept = False):
        data = []
        set_rows = 0
        set_cols = 0
        try:
            with open(path) as csvfile:
                csvData = csv.reader(csvfile)
                for row in csvData:
                    data.append(row)
                    set_rows = set_rows + 1
                    if len(row) > set_cols:
                        set_cols = len(row)
        except IOError as e:
            print("({})".format(e))
            if notifyExcept:
                QMessageBox.critical(self, "Open failed", str(e), QMessageBox.Close)
            return

        rows = self.quickSendTable.rowCount()
        cols = self.quickSendTable.columnCount()
        # clear table
        for col in range(cols):
            for row in range(rows):
                self.quickSendTable.setItem(row, col, QTableWidgetItem(""))

        self._csvFilePath = path
        if (cols - 1) < set_cols:   # first colume is used by the "send" buttons.
            cols = set_cols + 10
            self.quickSendTable.setColumnCount(cols)
        if rows < set_rows:
            rows = set_rows + 20
            self.quickSendTable.setRowCount(rows)

        for row, rowdat in enumerate(data):
            if len(rowdat) > 0:
                for col, cell in enumerate(rowdat, 1):
                    self.quickSendTable.setItem(row, col, QTableWidgetItem(str(cell)))

        self.quickSendTable.resizeColumnsToContents()
        #self.quickSendTable.resizeRowsToContents()

    def sendTableRow(self, row):
        cols = self.quickSendTable.columnCount()
        try:
            data = ['0' + self.quickSendTable.item(row, col).text()
                for col in range(1, cols)
                if self.quickSendTable.item(row, col) is not None
                    and self.quickSendTable.item(row, col).text() is not '']
        except:
            print("Exception in get table data(row = %d)" % (row + 1))
        else:
            tmp = [d[-2] + d[-1] for d in data if len(d) >= 2]
            for t in tmp:
                if not is_hex(t):
                    QMessageBox.critical(self, "Error",
                        "'%s' is not hexadecimal." % (t), QMessageBox.Close)
                    return

            h = [int(t, 16) for t in tmp]
            self.transmitHex(h)

    def sendHex(self):
        hexStr = self.txtEdtInput.toPlainText()
        hexStr = ''.join(hexStr.split(" "))

        hexarray = []
        for i in range(0, len(hexStr), 2):
            hexarray.append(int(hexStr[i:i+2], 16))

        self.transmitHex(hexarray)

    def readerExcept(self, e):
        self.closePort()
        QMessageBox.critical(self, "Read failed", str(e), QMessageBox.Close)

    def timestamp(self):
        return datetime.datetime.now().time().isoformat()[:-3]

    def receive(self, data):
        self.appendOutputText("\n%s R<-:%s" % (self.timestamp(), data))

    def appendOutputText(self, data, color=Qt.black):
        # the qEditText's "append" methon will add a unnecessary newline.
        # self.txtEdtOutput.append(data.decode('utf-8'))

        tc=self.txtEdtOutput.textColor()
        self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End)
        self.txtEdtOutput.setTextColor(QtGui.QColor(color))
        self.txtEdtOutput.insertPlainText(data)
        self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End)
        self.txtEdtOutput.setTextColor(tc)

    def transmitHex(self, hexarray):
        if len(hexarray) > 0:
            byteArray = bytearray(hexarray)
            if self.serialport.isOpen():
                try:
                    self.serialport.write(byteArray)
                except serial.SerialException as e:
                    print("Exception in transmitHex(%s)" % repr(hexarray))
                    QMessageBox.critical(self, "Exception in transmitHex", str(e),
                        QMessageBox.Close)
                else:
                    # self.txCount += len( b )
                    # self.frame.statusbar.SetStatusText('Tx:%d' % self.txCount, 2)

                    text = ''.join(['%02X ' % i for i in hexarray])
                    self.appendOutputText("\n%s T->:%s" % (self.timestamp(), text),
                        Qt.blue)

    def GetPort(self):
        return self.cmbPort.currentText()

    def GetDataBits(self):
        s = self.cmbDataBits.currentText()
        if s == '5':
            return serial.FIVEBITS
        elif s == '6':
            return serial.SIXBITS
        elif s == '7':
            return serial.SEVENBITS
        elif s == '8':
            return serial.EIGHTBITS

    def GetParity(self):
        s = self.cmbParity.currentText()
        if s == 'None':
            return serial.PARITY_NONE
        elif s == 'Even':
            return serial.PARITY_EVEN
        elif s == 'Odd':
            return serial.PARITY_ODD
        elif s == 'Mark':
            return serial.PARITY_MARK
        elif s == 'Space':
            return serial.PARITY_SPACE

    def GetStopBits(self):
        s = self.cmbStopBits.currentText()
        if s == '1':
            return serial.STOPBITS_ONE
        elif s == '1.5':
            return serial.STOPBITS_ONE_POINT_FIVE
        elif s == '2':
            return serial.STOPBITS_TWO

    def openPort(self):
        if self.serialport.isOpen():
            return

        _port = self.GetPort()
        if '' == _port:
            QMessageBox.information(self, "Invalid parameters", "Port is empty.")
            return

        _baudrate = self.cmbBaudRate.currentText()
        if '' == _baudrate:
            QMessageBox.information(self, "Invalid parameters", "Baudrate is empty.")
            return

        self.serialport.port     = _port
        self.serialport.baudrate = _baudrate
        self.serialport.bytesize = self.GetDataBits()
        self.serialport.stopbits = self.GetStopBits()
        self.serialport.parity   = self.GetParity()
        self.serialport.rtscts   = self.chkRTSCTS.isChecked()
        self.serialport.xonxoff  = self.chkXonXoff.isChecked()
        # self.serialport.timeout  = THREAD_TIMEOUT
        # self.serialport.writeTimeout = SERIAL_WRITE_TIMEOUT
        try:
            self.serialport.open()
        except serial.SerialException as e:
            QMessageBox.critical(self, "Could not open serial port", str(e),
                QMessageBox.Close)
        else:
            self._start_reader()
            self.setWindowTitle("%s on %s [%s, %s%s%s%s%s]" % (
                appInfo.title,
                self.serialport.portstr,
                self.serialport.baudrate,
                self.serialport.bytesize,
                self.serialport.parity,
                self.serialport.stopbits,
                self.serialport.rtscts and ' RTS/CTS' or '',
                self.serialport.xonxoff and ' Xon/Xoff' or '',
                )
            )
            pal = self.btnOpen.palette()
            pal.setColor(QtGui.QPalette.Button, QtGui.QColor(0,0xff,0x7f))
            self.btnOpen.setAutoFillBackground(True)
            self.btnOpen.setPalette(pal)
            self.btnOpen.setText('Close')
            self.btnOpen.update()

    def closePort(self):
        if self.serialport.isOpen():
            self._stop_reader()
            self.serialport.close()
            self.setWindowTitle(appInfo.title)
            pal = self.btnOpen.style().standardPalette()
            self.btnOpen.setAutoFillBackground(True)
            self.btnOpen.setPalette(pal)
            self.btnOpen.setText('Open')
            self.btnOpen.update()

    def _start_reader(self):
        """Start reader thread"""
        self.receiver_thread.start()

    def _stop_reader(self):
        """Stop reader thread only, wait for clean exit of thread"""
        self.receiver_thread.join()

    def onTogglePrtCfgPnl(self):
        if self.actionPort_Config_Panel.isChecked():
            self.dockWidget_PortConfig.show()
        else:
            self.dockWidget_PortConfig.hide()

    def onToggleQckSndPnl(self):
        if self.actionQuick_Send_Panel.isChecked():
            self.dockWidget_QuickSend.show()
        else:
            self.dockWidget_QuickSend.hide()

    def onToggleHexPnl(self):
        if self.actionSend_Hex_Panel.isChecked():
            self.dockWidget_SendHex.show()
        else:
            self.dockWidget_SendHex.hide()

    def onVisiblePrtCfgPnl(self, visible):
        self.actionPort_Config_Panel.setChecked(visible)

    def onVisibleQckSndPnl(self, visible):
        self.actionQuick_Send_Panel.setChecked(visible)

    def onVisibleHexPnl(self, visible):
        self.actionSend_Hex_Panel.setChecked(visible)

    def onLocalEcho(self):
        self._localEcho = self.actionLocal_Echo.isChecked()

    def onAlwaysOnTop(self):
        if self.actionAlways_On_Top.isChecked():
            style = self.windowFlags()
            self.setWindowFlags(style|Qt.WindowStaysOnTopHint)
            self.show()
        else:
            style = self.windowFlags()
            self.setWindowFlags(style & ~Qt.WindowStaysOnTopHint)
            self.show()

    def onOpen(self):
        if self.serialport.isOpen():
            self.closePort()
        else:
            self.openPort()

    def onClear(self):
        self.txtEdtOutput.clear()

    def onSaveLog(self):
        fileName = QFileDialog.getSaveFileName(self, "Save as", os.getcwd(),
            "Log files (*.log);;Text files (*.txt);;All files (*.*)")[0]
        if fileName:
            import codecs
            f = codecs.open(fileName, 'w', 'utf-8')
            f.write(self.txtEdtOutput.toPlainText())
            f.close()

    def moveScreenCenter(self):
        w = self.frameGeometry().width()
        h = self.frameGeometry().height()
        desktop = QDesktopWidget()
        screenW = desktop.screen().width()
        screenH = desktop.screen().height()
        self.setGeometry((screenW-w)/2, (screenH-h)/2, w, h)

    def onEnumPorts(self):
        for p in enum_ports():
            self.cmbPort.addItem(p)
        # self.cmbPort.update()

    def onAbout(self):
        q = QWidget()
        icon = QtGui.QIcon(":/icon.ico")
        q.setWindowIcon(icon)
        QMessageBox.about(q, "About MyTerm", appInfo.aboutme)

    def onAboutQt(self):
        QMessageBox.aboutQt(None)

    def onExit(self):
        if self.serialport.isOpen():
            self.closePort()
        self.close()

    def restoreLayout(self):
        if os.path.isfile(get_config_path("layout.dat")):
            try:
                f=open(get_config_path("layout.dat"), 'rb')
                geometry, state=pickle.load(f)
                self.restoreGeometry(geometry)
                self.restoreState(state)
            except Exception as e:
                print("Exception on restoreLayout, {}".format(e))
        else:
            try:
                f=QFile(':/default_layout.dat')
                f.open(QIODevice.ReadOnly)
                geometry, state=pickle.loads(f.readAll())
                self.restoreGeometry(geometry)
                self.restoreState(state)
            except Exception as e:
                print("Exception on restoreLayout, {}".format(e))

    def saveLayout(self):
        with open(get_config_path("layout.dat"), 'wb') as f:
            pickle.dump((self.saveGeometry(), self.saveState()), f)

    def syncMenu(self):
        self.actionPort_Config_Panel.setChecked(not self.dockWidget_PortConfig.isHidden())
        self.actionQuick_Send_Panel.setChecked(not self.dockWidget_QuickSend.isHidden())
        self.actionSend_Hex_Panel.setChecked(not self.dockWidget_SendHex.isHidden())

    def onViewChanged(self):
        checked = self._viewGroup.checkedAction()
        if checked is None:
            self.actionHEX_UPPERCASE.setChecked(True)
            self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE)
        else:
            if 'Ascii' in checked.text():
                self.receiver_thread.setViewMode(VIEWMODE_ASCII)
            elif 'lowercase' in checked.text():
                self.receiver_thread.setViewMode(VIEWMODE_HEX_LOWERCASE)
            elif 'UPPERCASE' in checked.text():
                self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE)
예제 #6
0
class MineSweeperWindow(QMainWindow):
    def __init__(self, mode):
        super().__init__()
        self.ms = mode
        self.init_ui()
        self.set_menu()

    def init_ui(self):
        """初始化游戏界面"""
        # 1.确定游戏界面的标题,大小和背景颜色
        self.setObjectName('MainWindow')
        self.setWindowTitle('扫雷')
        self.setWindowIcon(QIcon(':/minesweeper.ico'))
        self.setFixedSize(50 * self.ms.length + 100, 50 * self.ms.width + 180)
        self.setStyleSheet('#MainWindow{background-color: #f6edd2}')
        self.remain_boom = QLCDNumber(2, self)
        self.remain_boom.move(50, 50)
        self.remain_boom.setFixedSize(60, 50)
        self.remain_boom.setStyleSheet(
            "border: 2px solid blue; color: red; background: black;")
        self.remain_boom.display(
            '{:>02d}'.format(self.ms.b_num if self.ms.b_num >= 0 else 0))
        self.timer = QBasicTimer()
        self.second = 0
        self.time = QLCDNumber(3, self)
        self.time.move(50 * self.ms.length - 40, 50)
        self.time.setFixedSize(90, 50)
        self.time.setStyleSheet(
            "border: 2px solid blue; color: red; background: black;")
        self.time.display('{:>03d}'.format(self.second))
        self.btn = QPushButton(self)
        self.btn.move(25 * self.ms.length + 20, 50)
        self.btn.setFixedSize(50, 50)
        self.btn.setIcon(QIcon(':/普通.png'))
        self.btn.setIconSize(QSize(45, 45))
        self.btn.setStyleSheet('QPushButton{border:None}')
        self.btn.clicked.connect(self.restart)
        self.over_signal = 0
        self.rank = sqlite3.connect('rank.db')
        self.c = self.rank.cursor()

    def set_menu(self):
        bar = self.menuBar()
        game = bar.addMenu('游戏(&G)')
        more_info = bar.addMenu('更多(&M)')

        new_game = QAction('新游戏(&N)', self)
        new_game.setShortcut('Ctrl+N')
        new_game.triggered.connect(self.start)
        game.addAction(new_game)
        restart = QAction('重玩(&R)', self)
        restart.setShortcut('Ctrl+R')
        restart.triggered.connect(self.restart)
        game.addAction(restart)
        game.addSeparator()
        self.modes = QActionGroup(self)
        self.easy = QAction('简单(E)', self)
        self.easy.setCheckable(True)
        game.addAction(self.modes.addAction(self.easy))
        self.medium = QAction('中等(M)', self)
        self.medium.setCheckable(True)
        game.addAction(self.modes.addAction(self.medium))
        self.hard = QAction('困难(H)', self)
        self.hard.setCheckable(True)
        game.addAction(self.modes.addAction(self.hard))
        self.modes.triggered.connect(
            lambda: self.set_mode(self.modes.checkedAction()))
        if isinstance(self.ms, EasyMode):
            self.easy.setChecked(True)
        elif isinstance(self.ms, MediumMode):
            self.medium.setChecked(True)
        elif isinstance(self.ms, HardMode):
            self.hard.setChecked(True)

        rank = QAction('排行耪(&R)', self)
        rank.triggered.connect(self.show_rank)
        more_info.addAction(rank)

    def paintEvent(self, e):
        """绘制游戏内容"""
        qp = QPainter()

        def draw_map():
            """绘制版面"""
            # background = QPixmap(':/背景2.png')
            # qp.drawPixmap(self.rect(), background)
            # qp.setBrush(QColor('#e8ff9d'))
            # qp.drawRect(50, 130, 50 * self.ms.length, 50 * self.ms.width)
            for x in range(0, self.ms.length, 2):
                for y in range(0, self.ms.width, 2):
                    qp.setBrush(QColor('#007a15'))
                    qp.drawRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50)
                for y in range(1, self.ms.width, 2):
                    qp.setBrush(QColor('#00701c'))
                    qp.drawRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50)
            for x in range(1, self.ms.length, 2):
                for y in range(0, self.ms.width, 2):
                    qp.setBrush(QColor('#00701c'))
                    qp.drawRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50)
                for y in range(1, self.ms.width, 2):
                    qp.setBrush(QColor('#007a15'))
                    qp.drawRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50)
            # qp.setPen(QPen(QColor(111, 108, 108), 2, Qt.SolidLine))
            # for x in range(self.ms.length + 1):
            #     qp.drawLine(50 * (x + 1), 130, 50 * (x + 1), 50 * self.ms.width + 130)
            # for y in range(self.ms.width + 1):
            #     qp.drawLine(50, 50 * (y + 1) + 80, 50 * self.ms.length + 50, 50 * (y + 1) + 80)

        def draw_blanks():
            qp.setBrush(QColor('#f4f4f4'))
            for x in range(self.ms.length):
                for y in range(self.ms.width):
                    if isinstance(self.ms.g_map[y][x], str):
                        if self.ms.g_map[y][x] == '0$' or self.ms.g_map[y][
                                x] == '1$':
                            # qp.setPen(QPen(QColor(219, 58, 58), 1, Qt.SolidLine))
                            # qp.setFont(QFont('Kai', 15))
                            flag = QPixmap(':/雷旗.png').scaled(50, 50)
                            qp.drawPixmap(
                                QRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50),
                                flag)
                            # qp.drawText(50 * (x + 1) + 18, 50 * (y + 1) + 115, '$')
                            continue
                        qp.setPen(QPen(QColor('black'), 1, Qt.SolidLine))
                        qp.setFont(QFont('Kai', 15))
                        qp.drawRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50)
                        if self.ms.g_map[y][x] == '0':
                            continue
                        if self.ms.g_map[y][x] == '*':
                            flag = QPixmap(':/土豆雷.png').scaled(50, 50)
                            qp.drawPixmap(
                                QRect(50 * (x + 1), 50 * (y + 1) + 80, 50, 50),
                                flag)
                            continue
                        qp.setPen(QPen(QColor('black'), 5, Qt.SolidLine))
                        qp.drawText(50 * (x + 1) + 18, 50 * (y + 1) + 115,
                                    '{}'.format(self.ms.g_map[y][x]))

        qp.begin(self)
        draw_map()
        draw_blanks()
        qp.end()

    def mousePressEvent(self, e):
        """根据鼠标的动作,确定落子位置"""
        if self.over_signal == 1:
            return
        if e.button() in (Qt.LeftButton, Qt.RightButton):
            mouse_x = e.windowPos().x()
            mouse_y = e.windowPos().y()
            if 50 <= mouse_x <= 50 * self.ms.length + 50 and 130 <= mouse_y <= 50 * self.ms.width + 130:
                if self.ms.step == 0:
                    self.timer.start(1000, self)
                    self.tic = time_ns()
                game_x = int(mouse_x // 50) - 1
                game_y = int((mouse_y - 80) // 50) - 1
            else:
                return
            if e.buttons() == Qt.LeftButton | Qt.RightButton:
                self.ms.click_around(game_x, game_y)
            elif e.buttons() == Qt.LeftButton:
                self.ms.click(game_x, game_y)
            else:
                self.ms.mark_mine(game_x, game_y)
        if self.ms.boom:
            self.timer.stop()
            self.btn.setIcon(QIcon(':/哭脸.png'))
            self.btn.setIconSize(QSize(45, 45))
            self.repaint(0, 0, 50 * self.ms.length + 100,
                         50 * self.ms.width + 180)
            self.over_signal = 1
            return
        elif self.ms.game_judge():
            self.timer.stop()
            self.toc = time_ns()
            self.btn.setIconSize(QSize(45, 45))
            self.btn.setIcon(QIcon(':/笑脸.png'))
            self.repaint(0, 0, 50 * self.ms.length + 100,
                         50 * self.ms.width + 180)
            self.check_rank()
            self.over_signal = 1
            return
        self.repaint(0, 0, 50 * self.ms.length + 100, 50 * self.ms.width + 180)
        self.remain_boom.display(
            '{:>02d}'.format(self.ms.b_num if self.ms.b_num >= 0 else 0))

    def timerEvent(self, e) -> None:
        self.second += 1
        self.time.display('{:>03d}'.format(self.second))

    def set_mode(self, action: QAction):
        if action == self.easy:
            self.close()
            self.msw = MineSweeperWindow(EasyMode())
            self.msw.show()
        elif action == self.medium:
            self.close()
            self.msw = MineSweeperWindow(MediumMode())
            self.msw.show()
        elif action == self.hard:
            self.close()
            self.msw = MineSweeperWindow(HardMode())
            self.msw.show()

    def show_rank(self):
        self.sk = ShowRank()
        self.sk.show()

    def start(self):
        self.close()
        self.a = Start()
        self.a.show()

    def restart(self):
        self.ms.refresh()
        self.repaint()
        self.btn.setIcon(QIcon(':/普通.png'))
        self.remain_boom.display(
            '{:>02d}'.format(self.ms.b_num if self.ms.b_num >= 0 else 0))
        self.second = 0
        self.timer.stop()
        self.time.display('{:>03d}'.format(self.second))
        self.over_signal = 0

    def check_rank(self):
        a_num = (self.toc - self.tic) / 10**9
        out = subprocess.check_output("whoami").decode("gbk")
        name = re.search(r"\\(.+)\r\n", out)
        a_user = name.group(1)
        for i in range(5, 0, -1):
            if isinstance(self.ms, EasyMode):
                mode = "Easy"
            elif isinstance(self.ms, MediumMode):
                mode = "Medium"
            elif isinstance(self.ms, HardMode):
                mode = "Hard"
            else:
                return
            self.c.execute("SELECT * FROM {} WHERE id=?;".format(mode), (i, ))
            feedback = self.c.fetchone()
            if i == 5:
                if (not feedback[2]) or (feedback[2] > a_num):
                    a_user, _ = QInputDialog.getText(self,
                                                     "用户名",
                                                     "请输入用户名:",
                                                     QLineEdit.Normal,
                                                     text=a_user)
                    self.c.execute(
                        "UPDATE {} SET user=?, time=? WHERE id=?;".format(
                            mode), (a_user, a_num, i))
                    self.rank.commit()
                    continue
                else:
                    return
            else:
                if (not feedback[2]) or (feedback[2] > a_num):
                    self.c.execute(
                        "UPDATE {0} "
                        "SET user = (SELECT user FROM {0} WHERE id=?), "
                        "time = (SELECT time FROM {0} WHERE id=?)"
                        "WHERE id=? ;".format(mode), (i, i, i + 1))
                    self.c.execute(
                        "UPDATE {} SET user=?, time=? WHERE id=? ;".format(
                            mode), (a_user, a_num, i))
                    self.rank.commit()
                else:
                    return

    def closeEvent(self, e):
        self.rank.commit()
        self.rank.close()