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()
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()
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])
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()
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)
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()