def __init__(self): super(MainEditor, self).__init__() self.setWindowTitle('Lex Talionis Palette Editor v5.9.0') self.setMinimumSize(640, 480) self.grid = QGridLayout() self.setLayout(self.grid) self.main_view = MainView(self) self.menu_bar = QMenuBar(self) self.palette_list = PaletteList.PaletteList(self) self.image_map_list = ImageMap.ImageMapList() self.scripts = [] self.undo_stack = QUndoStack(self) self.create_menu_bar() self.grid.setMenuBar(self.menu_bar) self.grid.addWidget(self.main_view, 0, 0) self.grid.addWidget(self.palette_list, 0, 1, 2, 1) self.info_form = QFormLayout() self.grid.addLayout(self.info_form, 1, 0) self.create_info_bars() self.clear_info()
def __init__(self): super().__init__() self.resize(600, 400) self.mUndoStack = QUndoStack(self) self.mUndoStack.setUndoLimit(100) self.mUndoStack.canUndoChanged.connect(self.can_undo_changed) self.mUndoStack.canRedoChanged.connect(self.can_redo_changed) self.actionUndo = self.menuBar().addAction("Undo") self.actionUndo.triggered.connect(self.mUndoStack.undo) self.actionRedo = self.menuBar().addAction("Redo") self.actionRedo.triggered.connect(self.mUndoStack.redo) self.can_undo_changed(self.mUndoStack.canUndo()) self.can_redo_changed(self.mUndoStack.canRedo()) self.image = QImage(self.size(), QImage.Format_ARGB32) self.image.fill(Qt.transparent) self.start_pos = None self.end_pos = None self.is_pressed = False
def __init__(self): QWidget.__init__(self) self.filename = "" self.undoStack = QUndoStack() self.undo = FlatButton(":/images/undo_normal.png", ":/images/undo_hover.png", "", ":/images/undo_disabled.png") self.redo = FlatButton(":/images/redo_normal.png", ":/images/redo_hover.png", "", ":/images/redo_disabled.png") self.undo.setToolTip("Undo") self.redo.setToolTip("Redo") self.undo.setEnabled(False) self.redo.setEnabled(False) hbox = QHBoxLayout() hbox.addStretch(0) hbox.addWidget(self.undo) hbox.addWidget(self.redo) self.titleLabel = QLabel() fnt = self.titleLabel.font() fnt.setPointSize(20) fnt.setBold(True) self.titleLabel.setFont(fnt) self.layout = QGridLayout() self.layout.addWidget(self.titleLabel, 0, 0) self.layout.addLayout(hbox, 0, 2) self.setLayout(self.layout)
def __init__(self, document, mu_page, index): assert isinstance(document, GMDoc) assert isinstance(mu_page, fitz.Page) assert type(index) is int self._doc = document self._index = index self._mu_page = mu_page # zoom in to get higher resolution pixmap # todo: add this param to preferences dialog zoom = 2 matrix = fitz.Matrix(zoom, zoom) page_data = self._mu_page.getPixmap(matrix=matrix, alpha=False).getImageData() self._undo_stack = QUndoStack() self._page_image = QPixmap() self._page_image.loadFromData(page_data) self._drawing_image = generateDrawingPixmap(self._page_image) self._last_drawing_hash = None self.captureState()
def __init__(self, parent: QWidget=None): super().__init__( parent ) self.parentWidget = parent self.domainModel = Manager() self.undoStack = QUndoStack(self)
def __init__(self, parent: QWidget = None): super().__init__(parent) self.parentWidget = parent self.userContainer = UserContainer() ## user data self.undoStack = QUndoStack(self)
def __init__(self, application): super().__init__() self._loading = True self.application = application self.settings = Settings(self) self.workflows = Workflows(self) self.undo_stack = QUndoStack(self) self.refresh_timer = QTimer() self._init_widgets() self.main_window_actions = MainWindowActions(self) self._init_window() self._init_toolbars() self.settings.restore() if self.settings.signature_key is None: self.settings.signature_key = genkey() self.show() self._update_action_toggles() # Update the GUI so that everything matches the model cell_attributes = self.grid.model.code_array.cell_attributes attributes = cell_attributes[self.grid.current] self.on_gui_update(attributes) self._loading = False self._previous_window_state = self.windowState()
def __init__(self, mainwindow, parent=None): """ Initialize the diagram scene. :type mainwindow: MainWindow :type parent: QWidget """ super().__init__(parent) self.document = File(parent=self) self.guid = GUID(self) self.factory = ItemFactory(self) self.index = ItemIndex(self) self.meta = PredicateMetaIndex(self) self.undostack = QUndoStack(self) self.undostack.setUndoLimit(50) self.validator = OWL2Validator(self) self.mainwindow = mainwindow self.pasteOffsetX = Clipboard.PasteOffsetX self.pasteOffsetY = Clipboard.PasteOffsetY self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.mouseOverNode = None self.mousePressEdge = None self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = {} connect(self.sgnItemAdded, self.index.add) connect(self.sgnItemRemoved, self.index.remove)
def __init__(self, participants, parent=None): super().__init__(parent) self.controller = None # :type: CompareFrameController|GeneratorTabController self.protocol = None # type: ProtocolAnalyzer self.col_count = 0 self.row_count = 0 self.display_data = None # type: list[str] self.search_results = [] self.search_value = "" self._proto_view = 0 self._refindex = -1 self.first_messages = [] self.hidden_rows = set() self.is_writeable = False self.locked = False self.decode = True # False for Generator self.background_colors = defaultdict(lambda: None) self.bold_fonts = defaultdict(lambda: False) self.italic_fonts = defaultdict(lambda: False) self.text_colors = defaultdict(lambda: None) self.vertical_header_text = defaultdict(lambda: None) self.vertical_header_colors = defaultdict(lambda: None) self._diffs = defaultdict(set) # type: dict[int, set[int]] self.undo_stack = QUndoStack() self.__participants = participants
def __init__(self, data, parent=None): super().__init__(parent) self.undo_stack = QUndoStack(self) self.game_name = data["gameName"] self.sprite_pixel_palettes = PixelPalettes( data["sprites"], Source.SPRITE ) self.tile_pixel_palettes = PixelPalettes(data["tiles"], Source.TILE) self.sprite_color_palettes = ColorPalettes( data["spriteColorPalettes"], Source.SPRITE ) self.tile_color_palettes = ColorPalettes( data["tileColorPalettes"], Source.TILE ) self.sprite_pixel_palettes.batch_updated.connect( self.pix_batch_updated ) self.tile_pixel_palettes.batch_updated.connect(self.pix_batch_updated) self.sprite_color_palettes.color_changed.connect(self.col_pal_updated) self.tile_color_palettes.color_changed.connect(self.col_pal_updated) self.sprite_pixel_palettes.row_count_updated.connect( self.row_count_updated ) self.tile_pixel_palettes.row_count_updated.connect( self.row_count_updated ) self.sprite_color_palettes.name_changed.connect(self.col_pal_renamed) self.tile_color_palettes.name_changed.connect(self.col_pal_renamed) self.sprite_color_palettes.palette_added.connect(self.col_pal_added) self.tile_color_palettes.palette_added.connect(self.col_pal_added) self.sprite_color_palettes.palette_removed.connect( self.col_pal_removed ) self.tile_color_palettes.palette_removed.connect(self.col_pal_removed)
def __init__(self, parent): super(TableView, self).__init__(parent=parent) self.setShowGrid(False) self.setAlternatingRowColors(True) # self.setDragEnabled(True) # self.setDropIndicatorShown(True) # self.setDragDropOverwriteMode(False) # self.setAcceptDrops(True) # self.viewport().setAcceptDrops(True) # self.setDragDropMode(QAbstractItemView.InternalMove) # self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.ExtendedSelection) # self.verticalHeader().setMinimumWidth(40) # self.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) # QHeaderView.setsize self.setEditTriggers(QAbstractItemView.DoubleClicked) self.matches = [] self.undoStack = QUndoStack() self.dialog = SearchAndReplace(self) self.dialog.nextBotton.clicked.connect(self.nextMatch) self.dialog.caseSensitive.stateChanged.connect(self.resetMatches) self.dialog.regxp.stateChanged.connect(self.resetMatches) self.dialog.textToSearch.textChanged.connect(self.resetMatches) self.dialog.repalceBotton.clicked.connect(self.replace) self.dialog.repalceAllButton.clicked.connect(self.replaceAll) self.current_match = None
def __init__(self, model, *args): super(GridTableView, self).__init__(*args) #self.path = path self.model = model self.setModel(model) self.horizontalHeader().setContextMenuPolicy(Qt.CustomContextMenu) self.verticalHeader().setContextMenuPolicy(Qt.CustomContextMenu) self.horizontalHeader().customContextMenuRequested['QPoint'].connect( self.OnCustomContextMenuRequestedH) self.verticalHeader().customContextMenuRequested['QPoint'].connect( self.OnCustomContextMenuRequestedV) self.setItemDelegate(TableViewDelegate(self)) self.verticalHeader().setSectionsMovable(True) self.setRowHeight(1, 150) self.undoStack = QUndoStack() self.undoStackIndex = 0 self.undoStack.indexChanged.connect(self.OnUndoStackIndexChanged) self.setCornerButtonEnabled(False)
def _refund(self, child: PhysicalPrinting, undo_stack: QUndoStack): undo_stack.push(child.scene().get_cube_modification( add=child.values['tickets_payed'], remove=(child, ), position=child.pos() + QPoint(1, 1), closed_operation=True, ))
def __init__(self, parent): super(mylabel, self).__init__(parent) self.image = QImage() self.drawing = True self.lastPoint = QPoint() self.modified = False self.scribbling = False self.eraserSize = 5 #橡皮擦初始值 self.fontSize = 12 #字形初始值 self.setAcceptDrops(True) self.savetextedit = [] self.text = [] #紀錄文字 self.eraserPos = [] self.temp_img = 1 #紀錄圖片編號 self.numstack = [] self.i = 0 # 紀錄textedit self.eraserClicked = False self.textClicked = False self.clearClicked = False self.undoStack = QUndoStack() self.m_undoaction = self.undoStack.createUndoAction(self, self.tr("&Undo")) # self.m_undoaction.setShortcut('Ctrl+Z') self.addAction(self.m_undoaction) self.m_redoaction = self.undoStack.createRedoAction(self, self.tr("&Redo")) # self.m_redoaction.setShortcut('Ctrl+Y') self.addAction(self.m_redoaction)
def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) # undo # <http://doc.qt.io/qt-5/qtwidgets-tools-undoframework-example.html> self.undoStack = QUndoStack() self.ui.menuStyles.addSeparator() self.undoAction = self.undoStack.createUndoAction(self) self.undoAction.setShortcuts(QKeySequence.Undo) self.ui.menuStyles.addAction(self.undoAction) self.redoAction = self.undoStack.createRedoAction(self) self.redoAction.setShortcuts(QKeySequence.Redo) self.ui.menuStyles.addAction(self.redoAction) # constants self.chartPatternFilter = self.tr("ChartPattern (*.chartpattern)") try: self.XL = win32com.client.Dispatch("Excel.Application") except pythoncom.com_error: QMessageBox.warning(self, self.tr("Error"), self.tr("Excel isn't installed.")) sys.exit(0) # model self.pickedModel = models.ChartStyleModel(self) self.pickedDelegate = models.ChartPatternDelegate(self) self.ui.treeView.setModel(self.pickedModel) self.ui.treeView.setItemDelegate(self.pickedDelegate) if QT_VERSION_STR.startswith("5."): self.ui.treeView.header().setSectionsMovable(False) else: self.ui.treeView.header().setMovable(False) self.targetModel = QStringListModel([ self.tr("ActiveBook"), self.tr("ActiveSheet"), ], self) self.typeModel = QStringListModel([ self.tr("All Chart"), self.tr("Type of ActiveChart"), ], self) self.ui.comboBoxTarget.setModel(self.targetModel) self.ui.comboBoxType.setModel(self.typeModel) self.pickedModel.setUndoStack(self.undoStack) # general_progress self.general_progress = QProgressBar() self.ui.statusbar.addWidget(self.general_progress) self.general_progress.setVisible(False) # progress & progObj self.progress = QProgressBar() self.ui.statusbar.addWidget(self.progress) self.progress.setVisible(False) self.progObj = QtProgressObject() self.progObj.initialized.connect(self.on_progress_initialized) self.progObj.updated.connect(self.on_progress_updated) self.progObj.finished.connect(self.on_progress_finished)
def initUndo(self): self.undoStack = QUndoStack(self) shortcut = QShortcut(QKeySequence.Undo, self) shortcut.activated.connect(self.undoStack.undo) shortcut = QShortcut(QKeySequence.Redo, self) shortcut.activated.connect(self.undoStack.redo)
def __init__(self, filename=None): super(App, self).__init__() self.is_write = False self.backup_file = os.path.join(os.getcwd(), '.tmp.json') self.setWindowTitle(GUI_NAME) self.filedialog = FileDialog(self) # self.table = Table(self) self.address_space = {} self.info = InfoDialog(title=GUI_NAME, parent=self) self.threadpool = QThreadPool() self.data = {'project': '', 'top_module': '', 'blocks': []} self.undoStack = QUndoStack() self.treeUndoStack = QUndoStack() self.treeUndoStack.setUndoLimit(50) self.tree = BlockView(parent=self, cols=register_columns, blocks=self.data['blocks'], undoStack=self.treeUndoStack) self.table = FieldView(parent=self, cols=field_columns, items=[], undoStack=self.undoStack) self.tree.selectionChanged.connect(self.createTable) self.hbox = QHBoxLayout() splitter = QSplitter(Qt.Horizontal) splitter.addWidget(self.tree) splitter.addWidget(self.table) splitter.setStretchFactor(0, 2) splitter.setStretchFactor(1, 5) self.hbox.addWidget(splitter) self.setCentralWidget(QWidget(self)) self.tabs = TabLayout(self) self.tabs.setContentsMargins(0, 0, 0, 0) tab1 = QWidget(self) tab1.setLayout(self.hbox) self.analyzer = AnalyzerWapper(parent=self, cols=field_columns, address_space=self.address_space, reload=self.reload_address) self.tabs.setTab(tab1, title='RegisterProfile') self.tabs.setTab(self.analyzer, title='RegisterAnalyzer') self.setCentralWidget(self.tabs) self.menubar = self.menuBar() self.create_ui() if self.check_backup(): self.loadFiles([self.backup_file]) else: if filename: self.loadFiles([filename]) # self.table.tableSaved.connect(self.backUpFile) self.tree.addAnalyzerTrigger.connect(self.analyzer.add_analyzer) self.undoStack.indexChanged.connect(self.backUpFile)
def __init__(self, map, fileName=QString()): super().__init__() ## # The filename of a plugin is unique. So it can be used to determine # the right plugin to be used for saving or reloading the map. # The nameFilter of a plugin can not be used, since it's translatable. # The filename of a plugin must not change while maps are open using this # plugin. ## self.mReaderFormat = None self.mWriterFormat = None self.mExportFormat = None self.mSelectedArea = QRegion() self.mSelectedObjects = QList() self.mSelectedTiles = QList() self.mCurrentLayerIndex = 0 self.mLastSaved = QDateTime() self.mLastExportFileName = '' self.mFileName = fileName self.mMap = map self.mLayerModel = LayerModel(self) self.mCurrentObject = map ## Current properties object. ## self.mRenderer = None self.mMapObjectModel = MapObjectModel(self) self.mTerrainModel = TerrainModel(self, self) self.mUndoStack = QUndoStack(self) self.createRenderer() if (map.layerCount() == 0): _x = -1 else: _x = 0 self.mCurrentLayerIndex = _x self.mLayerModel.setMapDocument(self) # Forward signals emitted from the layer model self.mLayerModel.layerAdded.connect(self.onLayerAdded) self.mLayerModel.layerAboutToBeRemoved.connect( self.onLayerAboutToBeRemoved) self.mLayerModel.layerRemoved.connect(self.onLayerRemoved) self.mLayerModel.layerChanged.connect(self.layerChanged) # Forward signals emitted from the map object model self.mMapObjectModel.setMapDocument(self) self.mMapObjectModel.objectsAdded.connect(self.objectsAdded) self.mMapObjectModel.objectsChanged.connect(self.objectsChanged) self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved) self.mMapObjectModel.rowsInserted.connect( self.onMapObjectModelRowsInserted) self.mMapObjectModel.rowsRemoved.connect( self.onMapObjectModelRowsInsertedOrRemoved) self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved) self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved) self.mUndoStack.cleanChanged.connect(self.modifiedChanged) # Register tileset references tilesetManager = TilesetManager.instance() tilesetManager.addReferences(self.mMap.tilesets())
def resize(self, undo_stack: QUndoStack) -> None: dialog = GridResizeDialog(self) dialog.exec_() if dialog.accepted: undo_stack.push( GridResize( self, *reversed(dialog.get_values()), ) )
def __initialize(self): """ Private method to initialize some data. """ self.__loaded = False self.__bookmarkRootNode = None self.__toolbar = None self.__menu = None self.__bookmarksModel = None self.__commands = QUndoStack()
def __init__(self): super().__init__() self.undoStack = QUndoStack(self) self.setDragMode(QGraphicsView.RubberBandDrag) self.scene = QGraphicsScene() self.scene.setSceneRect(0, 0, 500, 500) self.setScene(self.scene) self.tool = 'Valinta' self.show() self.draw = DrawItem(self)
def __init__(self, r, c, set_title): super().__init__(r, c) self.set_title = set_title self.check_change = True self.header_bold = False self.undo_stack = QUndoStack(self) self.init_cells() self.init_ui() self.installEventFilter(self) self.init_undo_cell_edits() return None
def __init__(self, parent: QWidget = None): super().__init__(parent) self.parentWidget = parent self.dataContainer = DataContainer() self.undoStack = QUndoStack(self) self.markersChanged.connect(self.updateMarkersFavGroup) self.favsGrpChanged.connect(self.updateAllFavsGroup) self.favsChanged.connect(self.updateAllFavsGroup)
def __init__(self, parent=None): super().__init__(parent) self.change_stacks = {} self.stack = QUndoStack() self.fpath = None self.px = QPixmap() self.active_label = PangoGraphic() self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) self.tool = None self.tool_size = 10 self.full_clear()
def __init__(self, window, new=True): super().__init__() if new: self._editable = True self._wheelZoom = True self._zoomFactor = (1.1, 1.1) self._zoomLimit = 20 self._scale = (1.0, 1.0) self._window = window self.setDragMode(QGraphicsView.RubberBandDrag) self.setFocusPolicy(Qt.StrongFocus) self.setMouseTracking(True) self._undoStack = QUndoStack() self._undoView = None
def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (QWidget) """ super(IconEditorGrid, self).__init__(parent) self.setAttribute(Qt.WA_StaticContents) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.__curColor = Qt.black self.__zoom = 12 self.__curTool = self.Pencil self.__startPos = QPoint() self.__endPos = QPoint() self.__dirty = False self.__selecting = False self.__selRect = QRect() self.__isPasting = False self.__clipboardSize = QSize() self.__pasteRect = QRect() self.__undoStack = QUndoStack(self) self.__currentUndoCmd = None self.__image = QImage(32, 32, QImage.Format_ARGB32) self.__image.fill(qRgba(0, 0, 0, 0)) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) self.__compositingMode = QPainter.CompositionMode_SourceOver self.__lastPos = (-1, -1) self.__gridEnabled = True self.__selectionAvailable = False self.__initCursors() self.__initUndoTexts() self.setMouseTracking(True) self.__undoStack.canRedoChanged.connect(self.canRedoChanged) self.__undoStack.canUndoChanged.connect(self.canUndoChanged) self.__undoStack.cleanChanged.connect(self.__cleanChanged) self.imageChanged.connect(self.__updatePreviewPixmap) QApplication.clipboard().dataChanged.connect(self.__checkClipboard) self.__checkClipboard()
def update_column_count(self, parent: QtWidgets.QWidget, undo_stack: QUndoStack) -> None: amount, ok = QInputDialog.getInt( parent, 'Choose new column count', '', self._columns, 1, 64, ) if ok: undo_stack.push(SetColumnCount( self, amount, ))
def __init__(self, filepath: str = None, reset_settings: bool = False): """ :param filepath: File path for inital file to be opened :param reset_settings: Ignore stored `QSettings` and use defaults """ super().__init__() self._loading = True self.settings = Settings(self, reset_settings=reset_settings) self.workflows = Workflows(self) self.undo_stack = QUndoStack(self) self.refresh_timer = QTimer() self._init_widgets() self.main_window_actions = MainWindowActions(self) self._init_window() self._init_toolbars() self.settings.restore() if self.settings.signature_key is None: self.settings.signature_key = genkey() # Update recent files in the file menu self.menuBar().file_menu.history_submenu.update() # Update toolbar toggle checkboxes self.update_action_toggles() # Update the GUI so that everything matches the model cell_attributes = self.grid.model.code_array.cell_attributes attributes = cell_attributes[self.grid.current] self.on_gui_update(attributes) self._loading = False self._previous_window_state = self.windowState() # Open initial file if provided by the command line if filepath is not None: if self.workflows.filepath_open(filepath): self.workflows.update_main_window_title() else: msg = "File '{}' could not be opened.".format(filepath) self.statusBar().showMessage(msg)
def __init__(self, **kwargs): QGraphicsScene.__init__(self, **kwargs) # view linked to this scene, currently under mouse self.active_view = None # incremented for each move self.move_id = 0 self._last_checked_move_id = 0 self.signal_remove_selected_items.connect(self.remove_selected_items) self.move_restrictions_on = False """Whether objects moves should be restricted. .. glossary:: move restrictions Some displacements can be restricted to certain directions. If ``move_restrictions_on`` is True, then the displacements are constrained to be along the `x` or `y` direction. The actual direction (`x` or `y`) should be the closest to the mouse displacement. For instance, if the mouse movement is mainly horizontal, then a :class:`~.qt.handles.PointHandle` would move along `x` only. This requires to store the initial position, at the beginning of the displacement. This is done by :meth:`reset_move` slots. """ # http://www.informit.com/articles/article.aspx?p=1187104&seqNum=3 # recommends to set the parent # "so that PyQt is able to clean it up at the right time # when the dialog box is destroyed" # but then there is a cycle => possible crashes on exit ? self.undo_stack = QUndoStack(parent=self)
def __init__(self, renderScene): super(MainManager, self).__init__(); self.renderScene = renderScene; self.noPen = QPen(Qt.NoPen); self.bodies = {}; self.bodyInstances = []; self.nameIndex = {}; #Draw the axes self.axes = QGraphicsItemGroup(); renderScene.addItem(self.axes); xAxis = QGraphicsItemGroup(self.axes); s1, s2 = 10, 4 x1, y1, x2, y2 = -self.AXIS_LEN, 0, self.AXIS_LEN, 0 QGraphicsLineItem(x1, y1, x2, y2, xAxis); QGraphicsLineItem(x2-s1, y2-s2, x2, y2, xAxis); QGraphicsLineItem(x2-s1, y2+s2, x2, y2, xAxis); x1, y1, x2, y2 = 0, -self.AXIS_LEN, 0, self.AXIS_LEN yAxis = QGraphicsItemGroup(self.axes); QGraphicsLineItem(x1, y1, x2, y2, yAxis); QGraphicsLineItem(x2-s2, -(y2-s1), x2, -y2, yAxis); QGraphicsLineItem(x2+s2, -(y2-s1), x2, -y2, yAxis); grid = GridItem(0, 0, self.UNITS_PER_METER, 100); renderScene.addItem(grid); for o in [self.axes, grid]: o.setZValue(-1000); self.undoStack = QUndoStack(self);
def initUndoRedo(self): self.undoStack = QUndoStack() temp = QDir(os.path.join(QDir.tempPath(), "FlatSiteBuilder")) if temp.exists(): temp.removeRecursively() temp.setPath(QDir.tempPath()) temp.mkdir("FlatSiteBuilder")
def __init__(self, map, fileName = QString()): super().__init__() ## # The filename of a plugin is unique. So it can be used to determine # the right plugin to be used for saving or reloading the map. # The nameFilter of a plugin can not be used, since it's translatable. # The filename of a plugin must not change while maps are open using this # plugin. ## self.mReaderFormat = None self.mWriterFormat = None self.mExportFormat = None self.mSelectedArea = QRegion() self.mSelectedObjects = QList() self.mSelectedTiles = QList() self.mCurrentLayerIndex = 0 self.mLastSaved = QDateTime() self.mLastExportFileName = '' self.mFileName = fileName self.mMap = map self.mLayerModel = LayerModel(self) self.mCurrentObject = map ## Current properties object. ## self.mRenderer = None self.mMapObjectModel = MapObjectModel(self) self.mTerrainModel = TerrainModel(self, self) self.mUndoStack = QUndoStack(self) self.createRenderer() if (map.layerCount() == 0): _x = -1 else: _x = 0 self.mCurrentLayerIndex = _x self.mLayerModel.setMapDocument(self) # Forward signals emitted from the layer model self.mLayerModel.layerAdded.connect(self.onLayerAdded) self.mLayerModel.layerAboutToBeRemoved.connect(self.onLayerAboutToBeRemoved) self.mLayerModel.layerRemoved.connect(self.onLayerRemoved) self.mLayerModel.layerChanged.connect(self.layerChanged) # Forward signals emitted from the map object model self.mMapObjectModel.setMapDocument(self) self.mMapObjectModel.objectsAdded.connect(self.objectsAdded) self.mMapObjectModel.objectsChanged.connect(self.objectsChanged) self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved) self.mMapObjectModel.rowsInserted.connect(self.onMapObjectModelRowsInserted) self.mMapObjectModel.rowsRemoved.connect(self.onMapObjectModelRowsInsertedOrRemoved) self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved) self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved) self.mUndoStack.cleanChanged.connect(self.modifiedChanged) # Register tileset references tilesetManager = TilesetManager.instance() tilesetManager.addReferences(self.mMap.tilesets())
def __init__(self, parent, **kwargs): """ :param ModManager manager: :param kwargs: anything to pass on to base class :return: """ global Manager Manager = modmanager.Manager() # noinspection PyArgumentList super().__init__(parent=parent,**kwargs) self._parent = parent self.rootpath = None #type: str self.modname = None #type: str self.rootitem = None #type: QFSItem # the mod table has this stored on the custom view, # but we have no custom view for the file tree, so...here it is self.undostack = QUndoStack()
class ModFileTreeModel(QAbstractItemModel): """ A custom model that presents a view into the actual files saved within a mod's folder. It is vastly simplified compared to the QFileSystemModel, and only supports editing the state of the checkbox on each file or folder (though there is some neat trickery that propagates a check-action on a directory to all of its descendants) """ #TODO: calculate and inform the user of any file-conflicts that will occur in their mod-setup to help them decide what needs to be hidden. rootPathChanged = pyqtSignal(str) hasUnsavedChanges = pyqtSignal(bool) def __init__(self, parent, **kwargs): """ :param ModManager manager: :param kwargs: anything to pass on to base class :return: """ global Manager Manager = modmanager.Manager() # noinspection PyArgumentList super().__init__(parent=parent,**kwargs) self._parent = parent self.rootpath = None #type: str self.modname = None #type: str self.rootitem = None #type: QFSItem # the mod table has this stored on the custom view, # but we have no custom view for the file tree, so...here it is self.undostack = QUndoStack() @property def root_path(self): return self.rootpath @property def root_item(self): return self.rootitem @property def current_mod(self): return self.modname @property def has_unsaved_changes(self): return Manager.DB.in_transaction def setRootPath(self, path=None): """ Using this instead of a setter just for API-similarity with QFileSystemModel. That's the same reason rootPathChanged is emitted at the end of the method, as well. :param str path: the absolute filesystem path to the active mod's data folder. If passed as ``None``, the model is reset to empty """ if path == self.rootpath: return # commit any changes we've made so far self.save() # drop the undo stack self.undostack.clear() if path is None: # reset Model to show nothing self.beginResetModel() self.rootpath=None self.rootitem=None self.modname=None self.rootPathChanged.emit(path) self.endResetModel() elif check_path(path): # tells the view to get ready to redisplay its contents self.beginResetModel() self.rootpath = path self.modname = os.path.basename(path) self._setup_or_reload_tree() # tells the view it should get new # data from model & reset itself self.endResetModel() # emit notifier signal self.rootPathChanged.emit(path) def _setup_or_reload_tree(self): """ Loads thde data from the db and disk """ self._load_tree() # now mark hidden files self._mark_hidden_files() # this used to call resetModel() stuff, too, but I decided # this wasn't the place for that. It's a little barren now... def _load_tree(self): """ Build the tree from the rootitem :return: """ # name for this item is never actually seen self.rootitem = QFSItem(path="", name="data", parent=None) self.rootitem.load_children(self.rootpath, namefilter=lambda n: n.lower() == 'meta.ini') def _mark_hidden_files(self): hfiles = list(r['filepath'] for r in Manager.DB.select( "hiddenfiles", "filepath", where="directory = ?", params=(self.modname,) )) # only files (with their full paths relative to the root of # the mod directory) are in the hidden files list; thus we # need only compare files and not dirs to the list. As usual, # a directory's checkstate will be derived from its children for c in self.rootitem.iterchildren(True): if c.lpath in hfiles: c.checkState = Qt_Unchecked def getitem(self, index) -> QFSItem: """Extracts actual item from given index :param QModelIndex index: """ if index.isValid(): item = index.internalPointer() if item: return item return self.rootitem def item_from_path(self, path_parts): """ :param path_parts: a tuple where each element is an element in the filesystem path leading from the root item to the item :return: the item """ item = self.rootitem for p in path_parts: item = item[p] return item def columnCount(self, *args, **kwargs) -> int: """Dir/File Name(+checkbox), path to file, file conflicts """ # return 2 return len(COLUMNS) def rowCount(self, index=QModelIndex(), *args, **kwargs) -> int: """Number of children contained by the item referenced by `index` :param QModelIndex index: """ # return 0 until we actually have something to show return self.getitem(index).child_count if self.rootitem else 0 def headerData(self, section, orient, role=None): """Just one column, 'Name'. super() call should take care of the size hints &c. :param int section: :param orient: :param role: """ if orient == Qt.Horizontal and role==Qt.DisplayRole: return ColHeaders[section] # return "Name" return super().headerData(section, orient, role) def index(self, row, col, parent=QModelIndex(), *args, **kwargs): """ :param int row: :param int col: :param QModelIndex parent: :return: the QModelIndex that represents the item at (row, col) with respect to the given parent index. (or the root index if parent is invalid) """ parent_item = self.rootitem if parent.isValid(): parent_item = parent.internalPointer() child = parent_item[row] if child: return self.createIndex(row, col, child) return QModelIndex() def getIndexFromItem(self, item) -> QModelIndex: return self.createIndex(item.row, 0, item) # noinspection PyArgumentList @pyqtSlot('QModelIndex',name="parent", result = 'QModelIndex') def parent(self, child_index=QModelIndex()): if not child_index.isValid(): return QModelIndex() # get the parent FSItem from the reference stored in each FSItem parent = child_index.internalPointer().parent if not parent or parent is self.rootitem: return QModelIndex() # Every FSItem has a row attribute # which we use to create the index return self.createIndex(parent.row, 0, parent) # noinspection PyArgumentList @pyqtSlot(name='parent', result='QObject') def parent_of_self(self): return self._parent def flags(self, index): """ Flags are held at the item level; lookup and return them from the item referred to by the index :param QModelIndex index: """ # item = self.getitem(index) return self.getitem(index).itemflags def data(self, index, role=Qt.DisplayRole): """ We handle DisplayRole to return the filename, CheckStateRole to indicate whether the file has been hidden, and Decoration Role to return different icons for folders and files. :param QModelIndex index: :param role: """ item = self.getitem(index) col = index.column() if col == COL_PATH: if role == Qt.DisplayRole: #second column is path return item.parent.path + "/" elif col == COL_CONFLICTS: # third column is conflicts if role == Qt.DisplayRole and \ self.modname in Manager.mods_with_conflicting_files \ and item.lpath in Manager.file_conflicts: return "Yes" else: # column must be Name if role == Qt.DisplayRole: return item.name elif role == Qt_CheckStateRole: # hides the complexity of the tristate workings return item.checkState elif role == Qt.DecorationRole: return item.icon # noinspection PyTypeChecker def setData(self, index, value, role=Qt_CheckStateRole): """Only the checkStateRole can be edited in this model. Most of the machinery for that is in the QFSItem class :param QModelIndex index: :param value: :param role: """ if not index.isValid(): return False item = self.getitem(index) if role==Qt_CheckStateRole: item.checkState = value #triggers cascade if this a dir # if this item is the last checked/unchecked item in a dir, # make sure the change is propagated up through the parent # hierarchy, to make sure that no folders remain checked # when none of their descendants are. ancestor = self._get_highest_affected_ancestor(item, value) if ancestor is not item: index1 = self.getIndexFromItem(ancestor) else: index1 = index # using the "last_child_seen" value--which SHOULD be the most # "bottom-right" child that was just changed--to feed to # datachanged saves a lot of individual calls. Hopefully there # won't be any concurrency issues to worry about later on. # update the db with which files are now hidden self.update_db(index1, self.getIndexFromItem(QFSItem.last_child_seen)) return True return super().setData(index, value, role) def _get_highest_affected_ancestor(self, item, value): """worst name for a function ever but i can't think of better""" if item.parent and item.parent.children_checkState() == value: return self._get_highest_affected_ancestor(item.parent, value) else: return item # noinspection PyUnresolvedReferences def _send_data_through_proxy(self, index1, index2, *args): proxy = self._parent.model() #QSortFilterProxyModel proxy.dataChanged.emit(proxy.mapFromSource(index1), proxy.mapFromSource(index2), *args) def save(self): """ Commit any unsaved changes (currenlty just to hidden files) to the db and save the updated db state to disk """ if Manager.DB.in_transaction: Manager.DB.commit() Manager.save_hidden_files() self.hasUnsavedChanges.emit(False) def revert(self): """ Undo all changes made to the tree since the last save. """ self.beginResetModel() #SOOOO... # will a rollback/drop-the-undostack work here? # or is individually undoing everything (a bunch of savepoint- # rollbacks) better? I guess it depends on whether we want to # be able to define a "clean" point in the middle of a # transaction... Manager.DB.rollback() self.undostack.clear() # while self.undostack.canUndo() and not self.undostack.isClean(): # self.undostack.undo() self._setup_or_reload_tree() self.endResetModel() self.hasUnsavedChanges.emit(False) def update_db(self, start_index, final_index): """Make changes to database. NOTE: this does not commit them! That must be done separately :param start_index: index of the "top-left" affected item :param final_index: index of the "bottom-right" affected item """ cb = partial(self._send_data_through_proxy, start_index, final_index) self.undostack.push( ChangeHiddenFilesCommand(self.rootitem, os.path.basename(self.rootpath), post_redo_callback=cb, post_undo_callback=cb )) self.hasUnsavedChanges.emit(True)
class AbstractPage(QObject): sigChanged = pyqtSignal() def __init__(self, parent: MasterDocument): super().__init__() self._objs = set() self._parent = parent self.undoStack = QUndoStack() self._name = "untitled" @property def name(self): return self._name @name.setter def name(self, n): self._name = n self.sigChanged.emit() @property def parentDoc(self): return self._parent def isModified(self): return not self.undoStack.isClean() def doCommand(self, cmd): cmd.doc = self self.undoStack.push(cmd) self.sigChanged.emit() def objects(self, objType=None, exclude=None): if not objType and not exclude: return set(self._objs) if exclude is None: exclude = () if objType: return {obj for obj in self._objs if type(obj) is objType and not (type(obj) in exclude)} else: return {obj for obj in self._objs if not (type(obj) in exclude)} def hasObject(self, obj): return obj in self._objs def findObjsInRect(self, rect: QRect, objType=None): return {obj for obj in self.objects(objType) if rect.intersects(obj.bbox())} def findObjsNear(self, pt: QPoint, dist=1, objType=None): hitRect = QRect(pt.x()-dist/2, pt.y()-dist/2, dist, dist) return {obj for obj in self.findObjsInRect(hitRect, objType) if obj.testHit(pt, dist)} def addObj(self, obj): self._objs.add(obj) self.sigChanged.emit() def removeObj(self, obj): self._objs.remove(obj) self.sigChanged.emit() @pyqtSlot() def undo(self): self.undoStack.undo() self.sigChanged.emit() @pyqtSlot() def redo(self): self.undoStack.redo() self.sigChanged.emit() def fromXml(self, pageNode): raise NotImplementedError() def toXml(self, parentNode): raise NotImplementedError()
def __init__(self, parent=None): super(GRODelegate, self).__init__(parent) self.undoStack = QUndoStack(self) # This is for undo/redo
class GRODelegate(QStyledItemDelegate): def __init__(self, parent=None): super(GRODelegate, self).__init__(parent) self.undoStack = QUndoStack(self) # This is for undo/redo def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) def sizeHint(self, option, index): fm = option.fontMetrics if index.column() == residNum: return QSize(fm.width("9,999,999"), fm.height()) if index.column() == residName: text = index.model().data(index) document = QTextDocument() document.setDefaultFont(option.font) document.setHtml(text) return QSize(document.idealWidth() + 5, fm.height()) return QStyledItemDelegate.sizeHint(self, option, index) def createEditor(self, parent, option, index): # residNum,residNum_color, residName, atomName, atomNum, X,Y,Z if index.column() == residNum: spinbox = QSpinBox(parent) spinbox.setRange(1, 200000) spinbox.setSingleStep(1) spinbox.setAlignment(Qt.AlignRight | Qt.AlignVCenter) return spinbox elif index.column() == residName: combobox = QComboBox(parent) combobox.addItems(comboBoxList) combobox.insertSeparator(23) combobox.setEditable(True) return combobox elif index.column() == atomName: editor = QLineEdit(parent) editor.returnPressed.connect(self.commitAndCloseEditor) return editor elif index.column() == atomNum: spinbox = QSpinBox(parent) spinbox.setRange(1, 200000) spinbox.setSingleStep(1) spinbox.setAlignment(Qt.AlignRight | Qt.AlignVCenter) return spinbox elif index.column() in (X, Y, Z): ###this works dspinbox = QDoubleSpinBox(parent) dspinbox.setRange(-200000, 200000) dspinbox.setSingleStep(0.1) dspinbox.setAlignment(Qt.AlignRight | Qt.AlignVCenter) return dspinbox else: return QStyledItemDelegate.createEditor(self, parent, option, index) def commitAndCloseEditor(self): editor = self.sender() if isinstance(editor, (QTextEdit, QLineEdit)): comitDataSignal.emit(editor) closeEditorSignal.emit(editor) def setEditorData(self, editor, index): text = index.model().data(index, Qt.DisplayRole) if index.column() == residNum: if text is None: value = 0 elif isinstance(text, int): value = text else: value = int(re.sub(r"[., ]", "", text)) editor.setValue(value) # elif index.column() == residName: # editor.setText(text) elif index.column() == atomName: editor.setText(text) elif index.column() == atomNum: if text is None: value = 0 elif isinstance(text, int): value = text else: value = int(re.sub(r"[., ]", "", text)) editor.setValue(value) elif index.column() in (X, Y, Z): if text is None: value = 0 elif isinstance(text, int): value = text else: value = float(text) editor.setValue(value) else: QStyledItemDelegate.setEditorData(self, editor, index) def setModelData(self, editor, model, index): command = CommandElementChange(self, editor, model, index, "Change item value") self.undoStack.push(command)
class MainManager(QObject): DEFAULT_BODY_SIZE = 50; TRANSCOORD_X = 30; TRANSCOORD_Y = -30; AXIS_LEN = 250; UNITS_PER_METER = 30; bodiesLoaded = pyqtSignal(dict); # itemPositionChanged = pyqtSignal(QPointF); # itemScaleChanged = pyqtSignal(QPointF); def __init__(self, renderScene): super(MainManager, self).__init__(); self.renderScene = renderScene; self.noPen = QPen(Qt.NoPen); self.bodies = {}; self.bodyInstances = []; self.nameIndex = {}; #Draw the axes self.axes = QGraphicsItemGroup(); renderScene.addItem(self.axes); xAxis = QGraphicsItemGroup(self.axes); s1, s2 = 10, 4 x1, y1, x2, y2 = -self.AXIS_LEN, 0, self.AXIS_LEN, 0 QGraphicsLineItem(x1, y1, x2, y2, xAxis); QGraphicsLineItem(x2-s1, y2-s2, x2, y2, xAxis); QGraphicsLineItem(x2-s1, y2+s2, x2, y2, xAxis); x1, y1, x2, y2 = 0, -self.AXIS_LEN, 0, self.AXIS_LEN yAxis = QGraphicsItemGroup(self.axes); QGraphicsLineItem(x1, y1, x2, y2, yAxis); QGraphicsLineItem(x2-s2, -(y2-s1), x2, -y2, yAxis); QGraphicsLineItem(x2+s2, -(y2-s1), x2, -y2, yAxis); grid = GridItem(0, 0, self.UNITS_PER_METER, 100); renderScene.addItem(grid); for o in [self.axes, grid]: o.setZValue(-1000); self.undoStack = QUndoStack(self); def raiseItems(self): for item in self.renderScene.selectedItems(): item.setZValue(item.zValue() + 0.001); def lowerItems(self): for item in self.renderScene.selectedItems(): item.setZValue(item.zValue() - 0.001); def deleteSelected(self): delCmd = DeleteCommand(self.renderScene.selectedItems()) self.undoStack.push(delCmd); def handleMoveCommand(self, pos1, pos2): moveCmd = MoveCommand(self.renderScene.selectedItems(), pos1, pos2) self.undoStack.push(moveCmd) moveCmd.undo() #The default event implementation moved the object already, another move is wrong #I do this 'cause Qt just doesn't allow me to disable the automatic redo() when pushing def handleScaleCommand(self, scaleDelta): scaleCmd = ScaleCommand(self.renderScene.selectedItems(), scaleDelta); self.undoStack.push(scaleCmd); scaleCmd.undo(); def loadFromPBE(self, PBEFile): with open(PBEFile, "r") as f: baseDir = os.path.dirname(PBEFile); data = json.load(f); for body in data["rigidBodies"]: self.loadBody(body, baseDir); self.bodiesLoaded.emit(self.bodies); def trans(self, vertexDef): return QPointF(vertexDef["x"]*self.TRANSCOORD_X, vertexDef["y"]*self.TRANSCOORD_Y) def loadBody(self, bodyDef, baseDir): bodyConf = {}; if (bodyDef["imagePath"]): bodyConf["image"] = os.path.join(baseDir, bodyDef["imagePath"]) else: bodyConf["image"] = None; bodyConf["shapes"] = []; for shape in bodyDef["shapes"]: bodyConf["shapes"].append( { "type": shape["type"], "vertices": [self.trans(vertex) for vertex in shape["vertices"]] } ); self.bodies[bodyDef["name"]] = bodyConf; def duplicateItems(self): for item in self.renderScene.selectedItems(): newItem = self.cloneBody(item.bodyspecName, item.pos(), width=item.getMeterWidth()); item.setSelected(False); newItem.setSelected(True); def cloneBody(self, bodyspecName, dropPos, itemId=None, width=0): bodyDef = self.bodies[bodyspecName]; if not itemId: if bodyspecName not in self.nameIndex: self.nameIndex[bodyspecName] = 0; self.nameIndex[bodyspecName] += 1; itemId = "{}{}".format(bodyspecName, self.nameIndex[bodyspecName]); body = BodyItem(itemId, bodyspecName, 2); self.bodyInstances.append(body); body.setPos(dropPos); group = QGraphicsItemGroup(body); self.renderScene.addItem(body); width = width*self.UNITS_PER_METER or self.DEFAULT_BODY_SIZE; for shape in bodyDef["shapes"]: vertices = shape["vertices"]; if shape["type"] == "POLYGON": newItem = QGraphicsPolygonItem(QPolygonF(vertices)); if shape["type"] == "CIRCLE": p1, p2 = vertices radius = math.hypot(p2.x()-p1.x(), p2.y()-p1.y()); newItem = QGraphicsEllipseItem(p1.x()-radius, p1.y()-radius, radius*2, radius*2); pen = QPen(); pen.setWidth(0); newItem.setPen(pen); newItem.setParentItem(group); bounding = group.childrenBoundingRect(); imagePath = None; height = 0; if (bodyDef["image"]): imagePath = bodyDef["image"]; pixmap = QPixmap(imagePath); body.setPixmap(pixmap); pm = QGraphicsPixmapItem(pixmap.scaledToWidth(width), body); body.setImg(pm); pm.setFlags(QGraphicsItem.ItemStacksBehindParent); pm.setOffset(0, -pm.boundingRect().height()); group.setScale(width/self.TRANSCOORD_X); height = pm.boundingRect().height(); else: group.setScale(width/bounding.width()); height = bounding.height(); for item in body.childItems(): item.setPos(item.pos().x(), item.pos().y() + height) body.updateBorder(); return body; def save(self, file): with open(file, "w") as f: instancesDef = []; for inst in self.bodyInstances: if inst.deleted: continue; pos = inst.scenePos(); instancesDef.append({"id": inst.itemId, "bodyspec": inst.bodyspecName, "pos": {"x": pos.x()/self.UNITS_PER_METER, "y": pos.y()/self.UNITS_PER_METER}, "width": inst.getMeterWidth(), "height": inst.getMeterHeight() }); output = {"spec": self.bodies, "instances": instancesDef, "nameIndex": self.nameIndex}; json.dump(output, f, cls=MyJsonEncoder); def loadFile(self, file): with open(file, "r") as f: self.renderScene.clearInstancesOf(BodyItem); self.bodyInstances = []; data = json.load(f); self.bodies = data["spec"]; for name, body in self.bodies.items(): shapes = []; for shape in body["shapes"]: shape["vertices"]=[QPointF(vertex["x"], vertex["y"]) for vertex in shape["vertices"]]; shapes.append(shape); body["shapes"]=shapes; self.bodies[name] = body; for inst in data["instances"]: pos = inst["pos"]; self.cloneBody(inst["bodyspec"], QPointF(pos["x"]*self.UNITS_PER_METER, pos["y"]*self.UNITS_PER_METER), itemId=inst["id"], width=inst["width"]) self.bodiesLoaded.emit(self.bodies); self.nameIndex = data["nameIndex"];
def __init__(self, parent: MasterDocument): super().__init__() self._objs = set() self._parent = parent self.undoStack = QUndoStack() self._name = "untitled"
class TableModel(QAbstractTableModel): ALIGNMENT_CHAR = " " data_edited = pyqtSignal(int, int) vertical_header_color_status_changed = pyqtSignal(bool) def __init__(self, participants, parent=None): super().__init__(parent) self.controller = None # :type: CompareFrameController|GeneratorTabController self.protocol = None # type: ProtocolAnalyzer self.col_count = 0 self.row_count = 0 self.display_data = None # type: list[str] self.search_results = [] self.search_value = "" self._proto_view = 0 self._refindex = -1 self.first_messages = [] self.hidden_rows = set() self.is_writeable = False self.locked = False self.decode = True # False for Generator self.background_colors = defaultdict(lambda: None) self.bold_fonts = defaultdict(lambda: False) self.italic_fonts = defaultdict(lambda: False) self.text_colors = defaultdict(lambda: None) self.vertical_header_text = defaultdict(lambda: None) self.vertical_header_colors = defaultdict(lambda: None) self._diffs = defaultdict(set) # type: dict[int, set[int]] self.undo_stack = QUndoStack() self.__participants = participants @property def participants(self): return self.__participants @participants.setter def participants(self, value): self.__participants = value for msg in self.protocol.messages: if msg.participant not in self.__participants: msg.participant = None @property def proto_view(self): return self._proto_view @proto_view.setter def proto_view(self, value): self._proto_view = value if self._refindex >= 0: self._diffs = self.find_differences(self._refindex) self.update() def get_alignment_offset_at(self, index: int): f = 1 if self.proto_view == 0 else 4 if self.proto_view == 1 else 8 alignment_offset = int(math.ceil(self.protocol.messages[index].alignment_offset / f)) return alignment_offset def __pad_until_index(self, row: int, bit_pos: int): """ Pad message in given row with zeros until given column so user can enter values behind end of message :return: """ try: new_bits = array.array("B", [0] * max(0, bit_pos - len(self.protocol.messages[row]))) if len(new_bits) == 0: return True self.protocol.messages[row].plain_bits = self.protocol.messages[row].plain_bits + new_bits msg = self.protocol.messages[row] self.display_data[ row] = msg.plain_bits if self.proto_view == 0 else msg.plain_hex_array if self.proto_view == 1 else msg.plain_ascii_array except IndexError: return False return True def headerData(self, section: int, orientation, role=Qt.DisplayRole): if orientation == Qt.Vertical: if role == Qt.DisplayRole: return self.vertical_header_text[section] elif role == Qt.BackgroundColorRole: return self.vertical_header_colors[section] elif role == Qt.TextColorRole: color = self.vertical_header_colors[section] if color: red, green, blue = color.red(), color.green(), color.blue() return QColor("black") if (red * 0.299 + green * 0.587 + blue * 0.114) > 186 else QColor("white") else: return None return super().headerData(section, orientation, role) def update(self): self.locked = True if self.protocol.num_messages > 0: if self.decode: if self.proto_view == 0: self.display_data = [msg.decoded_bits for msg in self.protocol.messages] elif self.proto_view == 1: self.display_data = [msg.decoded_hex_array for msg in self.protocol.messages] elif self.proto_view == 2: self.display_data = [msg.decoded_ascii_array for msg in self.protocol.messages] else: # Generator Model if self.proto_view == 0: self.display_data = [msg.plain_bits for msg in self.protocol.messages] elif self.proto_view == 1: self.display_data = [msg.plain_hex_array for msg in self.protocol.messages] else: self.display_data = [msg.plain_ascii_array for msg in self.protocol.messages] visible_messages = [msg for i, msg in enumerate(self.display_data) if i not in self.hidden_rows] if len(visible_messages) == 0: self.col_count = 0 else: self.col_count = max(len(msg) + self.get_alignment_offset_at(i) for i, msg in enumerate(self.display_data) if i not in self.hidden_rows) if self._refindex >= 0: self._diffs = self.find_differences(self._refindex) else: self._diffs.clear() self.row_count = self.protocol.num_messages self.find_protocol_value(self.search_value) else: self.col_count = 0 self.row_count = 0 self.display_data = None # Cache background colors for performance self.refresh_bgcolors() self.refresh_fonts() # Will be overriden self.refresh_vertical_header() self.beginResetModel() self.endResetModel() self.locked = False def insert_column(self, index: int, rows: list): if self.protocol is None or not self.is_writeable: return insert_action = InsertColumn(self.protocol, index, rows, self.proto_view) self.undo_stack.push(insert_action) def columnCount(self, QModelIndex_parent=None, *args, **kwargs): return self.col_count def rowCount(self, QModelIndex_parent=None, *args, **kwargs): return self.row_count def refresh_bgcolors(self): self.background_colors.clear() label_colors = constants.LABEL_COLORS for i, message in enumerate(self.protocol.messages): for lbl in message.message_type: bg_color = label_colors[lbl.color_index] a = self.get_alignment_offset_at(i) start, end = message.get_label_range(lbl, self.proto_view, self.decode) for j in range(start, end): self.background_colors[i, j + a] = bg_color def refresh_fonts(self): """ Will be overriden :return: """ pass def refresh_vertical_header(self): self.vertical_header_colors.clear() self.vertical_header_text.clear() use_colors = False for i in range(self.row_count): try: participant = self.protocol.messages[i].participant except IndexError: participant = None if participant: self.vertical_header_text[i] = "{0} ({1})".format(i + 1, participant.shortname) self.vertical_header_colors[i] = constants.PARTICIPANT_COLORS[participant.color_index] use_colors = True else: self.vertical_header_text[i] = str(i + 1) self.vertical_header_color_status_changed.emit(use_colors) def data(self, index: QModelIndex, role=Qt.DisplayRole): if not index.isValid(): return None i = index.row() j = index.column() if role == Qt.DisplayRole and self.display_data: try: alignment_offset = self.get_alignment_offset_at(i) if j < alignment_offset: return self.ALIGNMENT_CHAR if self.proto_view == 0: return self.display_data[i][j - alignment_offset] elif self.proto_view == 1: return "{0:x}".format(self.display_data[i][j - alignment_offset]) elif self.proto_view == 2: return chr(self.display_data[i][j - alignment_offset]) except IndexError: return None elif role == Qt.TextAlignmentRole: if i in self.first_messages: return Qt.AlignHCenter + Qt.AlignBottom else: return Qt.AlignCenter elif role == Qt.BackgroundColorRole: return self.background_colors[i, j] elif role == Qt.FontRole: font = QFont() font.setBold(self.bold_fonts[i, j]) font.setItalic(self.italic_fonts[i, j]) return font elif role == Qt.TextColorRole: return self.text_colors[i, j] elif role == Qt.ToolTipRole: return self.get_tooltip(i, j) else: return None def get_tooltip(self, row: int, column: int) -> str: msg = self.protocol.messages[row] try: lbl = next(lbl for lbl in msg.message_type if column in range(*msg.get_label_range(lbl, self.proto_view, self.decode))) except StopIteration: return "" result = lbl.name if isinstance(lbl, ChecksumLabel): calculated_crc = lbl.calculate_checksum_for_message(msg, use_decoded_bits=self.decode) start, end = msg.get_label_range(lbl=lbl, view=0, decode=self.decode) bits = msg.decoded_bits if self.decode else msg.plain_bits color = "green" if bits[start:end] == calculated_crc else "red" expected = util.convert_bits_to_string(calculated_crc, self.proto_view) result += '<br><font color="{}">Expected <b>{}</b></font>'.format(color, expected) return result def setData(self, index: QModelIndex, value, role=Qt.DisplayRole): if role != Qt.EditRole: return True i = index.row() j = index.column() a = self.get_alignment_offset_at(i) j -= a hex_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f") if i >= len(self.protocol.messages): return False if self.proto_view == 0 and value in ("0", "1") and self.__pad_until_index(i, j + 1): self.protocol.messages[i][j] = bool(int(value)) self.display_data[i][j] = int(value) elif self.proto_view == 1 and value in hex_chars and self.__pad_until_index(i, (j + 1) * 4): converted_j = self.protocol.convert_index(j, 1, 0, self.decode, message_indx=i)[0] bits = "{0:04b}".format(int(value, 16)) for k in range(4): self.protocol.messages[i][converted_j + k] = bool(int(bits[k])) self.display_data[i][j] = int(value, 16) elif self.proto_view == 2 and len(value) == 1 and self.__pad_until_index(i, (j + 1) * 8): converted_j = self.protocol.convert_index(j, 2, 0, self.decode, message_indx=i)[0] bits = "{0:08b}".format(ord(value)) for k in range(8): self.protocol.messages[i][converted_j + k] = bool(int(bits[k])) self.display_data[i][j] = ord(value) else: return False self.data_edited.emit(i, j) return True def find_protocol_value(self, value): self.search_results.clear() if self.proto_view == 1: value = value.lower() self.search_value = value if len(value) == 0: return 0 for i, message in enumerate(self.protocol.messages): if i in self.hidden_rows: continue data = message.view_to_string(self.proto_view, self.decode) j = data.find(value) while j != -1: self.search_results.append((i, j + self.get_alignment_offset_at(i))) j = data.find(value, j + 1) return len(self.search_results) def find_differences(self, refindex: int): """ Search all differences between protocol messages regarding a reference message :param refindex: index of reference message :rtype: dict[int, set[int]] """ differences = defaultdict(set) if refindex >= len(self.protocol.messages): return differences if self.proto_view == 0: proto = self.protocol.decoded_proto_bits_str elif self.proto_view == 1: proto = self.protocol.decoded_hex_str elif self.proto_view == 2: proto = self.protocol.decoded_ascii_str else: return differences ref_message = proto[refindex] ref_offset = self.get_alignment_offset_at(refindex) for i, message in enumerate(proto): if i == refindex: continue msg_offset = self.get_alignment_offset_at(i) short, long = sorted([len(ref_message) + ref_offset, len(message) + msg_offset]) differences[i] = { j for j in range(max(msg_offset, ref_offset), long) if j >= short or message[j - msg_offset] != ref_message[j - ref_offset] } return differences def get_selected_label_index(self, row: int, column: int): if self.row_count == 0: return -1 try: msg = self.protocol.messages[row] except IndexError: logger.warning("{} is out of range for generator protocol".format(row)) return -1 for i, lbl in enumerate(msg.message_type): if column in range(*msg.get_label_range(lbl, self.proto_view, False)): return i return -1
class MapDocument(QObject): fileNameChanged = pyqtSignal(str, str) modifiedChanged = pyqtSignal() saved = pyqtSignal() ## # Emitted when the selected tile region changes. Sends the currently # selected region and the previously selected region. ## selectedAreaChanged = pyqtSignal(QRegion, QRegion) ## # Emitted when the list of selected objects changes. ## selectedObjectsChanged = pyqtSignal() ## # Emitted when the list of selected tiles from the dock changes. ## selectedTilesChanged = pyqtSignal() currentObjectChanged = pyqtSignal(list) ## # Emitted when the map size or its tile size changes. ## mapChanged = pyqtSignal() layerAdded = pyqtSignal(int) layerAboutToBeRemoved = pyqtSignal(int) layerRenamed = pyqtSignal(int) layerRemoved = pyqtSignal(int) layerChanged = pyqtSignal(int) ## # Emitted after a new layer was added and the name should be edited. # Applies to the current layer. ## editLayerNameRequested = pyqtSignal() editCurrentObject = pyqtSignal() ## # Emitted when the current layer index changes. ## currentLayerIndexChanged = pyqtSignal(int) ## # Emitted when a certain region of the map changes. The region is given in # tile coordinates. ## regionChanged = pyqtSignal(QRegion, Layer) ## # Emitted when a certain region of the map was edited by user input. # The region is given in tile coordinates. # If multiple layers have been edited, multiple signals will be emitted. ## regionEdited = pyqtSignal(QRegion, Layer) tileLayerDrawMarginsChanged = pyqtSignal(TileLayer) tileTerrainChanged = pyqtSignal(QList) tileProbabilityChanged = pyqtSignal(Tile) tileObjectGroupChanged = pyqtSignal(Tile) tileAnimationChanged = pyqtSignal(Tile) objectGroupChanged = pyqtSignal(ObjectGroup) imageLayerChanged = pyqtSignal(ImageLayer) tilesetAboutToBeAdded = pyqtSignal(int) tilesetAdded = pyqtSignal(int, Tileset) tilesetAboutToBeRemoved = pyqtSignal(int) tilesetRemoved = pyqtSignal(Tileset) tilesetMoved = pyqtSignal(int, int) tilesetFileNameChanged = pyqtSignal(Tileset) tilesetNameChanged = pyqtSignal(Tileset) tilesetTileOffsetChanged = pyqtSignal(Tileset) tilesetChanged = pyqtSignal(Tileset) objectsAdded = pyqtSignal(QList) objectsInserted = pyqtSignal(ObjectGroup, int, int) objectsRemoved = pyqtSignal(QList) objectsChanged = pyqtSignal(QList) objectsIndexChanged = pyqtSignal(ObjectGroup, int, int) propertyAdded = pyqtSignal(Object, str) propertyRemoved = pyqtSignal(Object, str) propertyChanged = pyqtSignal(Object, str) propertiesChanged = pyqtSignal(Object) ## # Constructs a map document around the given map. The map document takes # ownership of the map. ## def __init__(self, map, fileName = QString()): super().__init__() ## # The filename of a plugin is unique. So it can be used to determine # the right plugin to be used for saving or reloading the map. # The nameFilter of a plugin can not be used, since it's translatable. # The filename of a plugin must not change while maps are open using this # plugin. ## self.mReaderFormat = None self.mWriterFormat = None self.mExportFormat = None self.mSelectedArea = QRegion() self.mSelectedObjects = QList() self.mSelectedTiles = QList() self.mCurrentLayerIndex = 0 self.mLastSaved = QDateTime() self.mLastExportFileName = '' self.mFileName = fileName self.mMap = map self.mLayerModel = LayerModel(self) self.mCurrentObject = map ## Current properties object. ## self.mRenderer = None self.mMapObjectModel = MapObjectModel(self) self.mTerrainModel = TerrainModel(self, self) self.mUndoStack = QUndoStack(self) self.createRenderer() if (map.layerCount() == 0): _x = -1 else: _x = 0 self.mCurrentLayerIndex = _x self.mLayerModel.setMapDocument(self) # Forward signals emitted from the layer model self.mLayerModel.layerAdded.connect(self.onLayerAdded) self.mLayerModel.layerAboutToBeRemoved.connect(self.onLayerAboutToBeRemoved) self.mLayerModel.layerRemoved.connect(self.onLayerRemoved) self.mLayerModel.layerChanged.connect(self.layerChanged) # Forward signals emitted from the map object model self.mMapObjectModel.setMapDocument(self) self.mMapObjectModel.objectsAdded.connect(self.objectsAdded) self.mMapObjectModel.objectsChanged.connect(self.objectsChanged) self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved) self.mMapObjectModel.rowsInserted.connect(self.onMapObjectModelRowsInserted) self.mMapObjectModel.rowsRemoved.connect(self.onMapObjectModelRowsInsertedOrRemoved) self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved) self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved) self.mUndoStack.cleanChanged.connect(self.modifiedChanged) # Register tileset references tilesetManager = TilesetManager.instance() tilesetManager.addReferences(self.mMap.tilesets()) ## # Destructor. ## def __del__(self): # Unregister tileset references tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMap.tilesets()) del self.mRenderer del self.mMap ## # Saves the map to its current file name. Returns whether or not the file # was saved successfully. If not, <i>error</i> will be set to the error # message if it is not 0. ## def save(self, *args): l = len(args) if l==0: args = ('') if l==1: arg = args[0] file = QFileInfo(arg) if not file.isFile(): fileName = self.fileName() error = args[0] else: fileName = arg error = '' return self.save(fileName, error) if l==2: ## # Saves the map to the file at \a fileName. Returns whether or not the # file was saved successfully. If not, <i>error</i> will be set to the # error message if it is not 0. # # If the save was successful, the file name of this document will be set # to \a fileName. # # The map format will be the same as this map was opened with. ## fileName, error = args mapFormat = self.mWriterFormat tmxMapFormat = TmxMapFormat() if (not mapFormat): mapFormat = tmxMapFormat if (not mapFormat.write(self.map(), fileName)): if (error): error = mapFormat.errorString() return False self.undoStack().setClean() self.setFileName(fileName) self.mLastSaved = QFileInfo(fileName).lastModified() self.saved.emit() return True ## # Loads a map and returns a MapDocument instance on success. Returns 0 # on error and sets the \a error message. ## def load(fileName, mapFormat = None): error = '' tmxMapFormat = TmxMapFormat() if (not mapFormat and not tmxMapFormat.supportsFile(fileName)): # Try to find a plugin that implements support for this format formats = PluginManager.objects() for format in formats: if (format.supportsFile(fileName)): mapFormat = format break map = None errorString = '' if mapFormat: map = mapFormat.read(fileName) errorString = mapFormat.errorString() else: map = tmxMapFormat.read(fileName) errorString = tmxMapFormat.errorString() if (not map): error = errorString return None, error mapDocument = MapDocument(map, fileName) if mapFormat: mapDocument.setReaderFormat(mapFormat) if mapFormat.hasCapabilities(MapFormat.Write): mapDocument.setWriterFormat(mapFormat) return mapDocument, error def fileName(self): return self.mFileName def lastExportFileName(self): return self.mLastExportFileName def setLastExportFileName(self, fileName): self.mLastExportFileName = fileName def readerFormat(self): return self.mReaderFormat def setReaderFormat(self, format): self.mReaderFormat = format def writerFormat(self): return self.mWriterFormat def setWriterFormat(self, format): self.mWriterFormat = format def exportFormat(self): return self.mExportFormat def setExportFormat(self, format): self.mExportFormat = format ## # Returns the name with which to display this map. It is the file name without # its path, or 'untitled.tmx' when the map has no file name. ## def displayName(self): displayName = QFileInfo(self.mFileName).fileName() if len(displayName)==0: displayName = self.tr("untitled.tmx") return displayName ## # Returns whether the map has unsaved changes. ## def isModified(self): return not self.mUndoStack.isClean() def lastSaved(self): return self.mLastSaved ## # Returns the map instance. Be aware that directly modifying the map will # not allow the GUI to update itself appropriately. ## def map(self): return self.mMap ## # Sets the current layer to the given index. ## def setCurrentLayerIndex(self, index): changed = self.mCurrentLayerIndex != index self.mCurrentLayerIndex = index ## This function always sends the following signal, even if the index # didn't actually change. This is because the selected index in the layer # table view might be out of date anyway, and would otherwise not be # properly updated. # # This problem happens due to the selection model not sending signals # about changes to its current index when it is due to insertion/removal # of other items. The selected item doesn't change in that case, but our # layer index does. ## self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex) if (changed and self.mCurrentLayerIndex != -1): self.setCurrentObject(self.currentLayer()) ## # Returns the index of the currently selected layer. Returns -1 if no # layer is currently selected. ## def currentLayerIndex(self): return self.mCurrentLayerIndex ## # Returns the currently selected layer, or 0 if no layer is currently # selected. ## def currentLayer(self): if (self.mCurrentLayerIndex == -1): return None return self.mMap.layerAt(self.mCurrentLayerIndex) ## # Resize this map to the given \a size, while at the same time shifting # the contents by \a offset. ## def resizeMap(self, size, offset): movedSelection = self.mSelectedArea.translated(offset) newArea = QRect(-offset, size) visibleArea = self.mRenderer.boundingRect(newArea) origin = self.mRenderer.tileToPixelCoords_(QPointF()) newOrigin = self.mRenderer.tileToPixelCoords_(-offset) pixelOffset = origin - newOrigin # Resize the map and each layer self.mUndoStack.beginMacro(self.tr("Resize Map")) for i in range(self.mMap.layerCount()): layer = self.mMap.layerAt(i) x = layer.layerType() if x==Layer.TileLayerType: tileLayer = layer self.mUndoStack.push(ResizeTileLayer(self, tileLayer, size, offset)) elif x==Layer.ObjectGroupType: objectGroup = layer # Remove objects that will fall outside of the map for o in objectGroup.objects(): if (not visibleIn(visibleArea, o, self.mRenderer)): self.mUndoStack.push(RemoveMapObject(self, o)) else: oldPos = o.position() newPos = oldPos + pixelOffset self.mUndoStack.push(MoveMapObject(self, newPos, oldPos)) elif x==Layer.ImageLayerType: # Currently not adjusted when resizing the map break self.mUndoStack.push(ResizeMap(self, size)) self.mUndoStack.push(ChangeSelectedArea(self, movedSelection)) self.mUndoStack.endMacro() # TODO: Handle layers that don't match the map size correctly ## # Offsets the layers at \a layerIndexes by \a offset, within \a bounds, # and optionally wraps on the X or Y axis. ## def offsetMap(self, layerIndexes, offset, bounds, wrapX, wrapY): if (layerIndexes.empty()): return if (layerIndexes.size() == 1): self.mUndoStack.push(OffsetLayer(self, layerIndexes.first(), offset, bounds, wrapX, wrapY)) else: self.mUndoStack.beginMacro(self.tr("Offset Map")) for layerIndex in layerIndexes: self.mUndoStack.push(OffsetLayer(self, layerIndex, offset, bounds, wrapX, wrapY)) self.mUndoStack.endMacro() ## # Flips the selected objects in the given \a direction. ## def flipSelectedObjects(self, direction): if (self.mSelectedObjects.isEmpty()): return self.mUndoStack.push(FlipMapObjects(self, self.mSelectedObjects, direction)) ## # Rotates the selected objects. ## def rotateSelectedObjects(self, direction): if (self.mSelectedObjects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Rotate %n Object(s)", "", self.mSelectedObjects.size())) # TODO: Rotate them properly as a group for mapObject in self.mSelectedObjects: oldRotation = mapObject.rotation() newRotation = oldRotation if (direction == RotateDirection.RotateLeft): newRotation -= 90 if (newRotation < -180): newRotation += 360 else: newRotation += 90 if (newRotation > 180): newRotation -= 360 self.mUndoStack.push(RotateMapObject(self, mapObject, newRotation, oldRotation)) self.mUndoStack.endMacro() ## # Adds a layer of the given type to the top of the layer stack. After adding # the new layer, emits editLayerNameRequested(). ## def addLayer(self, layerType): layer = None name = QString() x = layerType if x==Layer.TileLayerType: name = self.tr("Tile Layer %d"%(self.mMap.tileLayerCount() + 1)) layer = TileLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x==Layer.ObjectGroupType: name = self.tr("Object Layer %d"%(self.mMap.objectGroupCount() + 1)) layer = ObjectGroup(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x==Layer.ImageLayerType: name = self.tr("Image Layer %d"%(self.mMap.imageLayerCount() + 1)) layer = ImageLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) index = self.mMap.layerCount() self.mUndoStack.push(AddLayer(self, index, layer)) self.setCurrentLayerIndex(index) self.editLayerNameRequested.emit() ## # Duplicates the currently selected layer. ## def duplicateLayer(self): if (self.mCurrentLayerIndex == -1): return duplicate = self.mMap.layerAt(self.mCurrentLayerIndex).clone() duplicate.setName(self.tr("Copy of %s"%duplicate.name())) index = self.mCurrentLayerIndex + 1 cmd = AddLayer(self, index, duplicate) cmd.setText(self.tr("Duplicate Layer")) self.mUndoStack.push(cmd) self.setCurrentLayerIndex(index) ## # Merges the currently selected layer with the layer below. This only works # when the layers can be merged. # # \see Layer.canMergeWith ## def mergeLayerDown(self): if (self.mCurrentLayerIndex < 1): return upperLayer = self.mMap.layerAt(self.mCurrentLayerIndex) lowerLayer = self.mMap.layerAt(self.mCurrentLayerIndex - 1) if (not lowerLayer.canMergeWith(upperLayer)): return merged = lowerLayer.mergedWith(upperLayer) self.mUndoStack.beginMacro(self.tr("Merge Layer Down")) self.mUndoStack.push(AddLayer(self, self.mCurrentLayerIndex - 1, merged)) self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex)) self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex)) self.mUndoStack.endMacro() ## # Moves the given layer up. Does nothing when no valid layer index is # given. ## def moveLayerUp(self, index): if index<0 or index>=self.mMap.layerCount() - 1: return self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Up)) ## # Moves the given layer down. Does nothing when no valid layer index is # given. ## def moveLayerDown(self, index): if index<1 or index>=self.mMap.layerCount(): return self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Down)) ## # Removes the given layer. ## def removeLayer(self, index): if index<0 or index>=self.mMap.layerCount(): return self.mUndoStack.push(RemoveLayer(self, index)) ## # Show or hide all other layers except the layer at the given index. # If any other layer is visible then all layers will be hidden, otherwise # the layers will be shown. ## def toggleOtherLayers(self, index): self.mLayerModel.toggleOtherLayers(index) ## # Adds a tileset to this map at the given \a index. Emits the appropriate # signal. ## def insertTileset(self, index, tileset): self.tilesetAboutToBeAdded.emit(index) self.mMap.insertTileset(index, tileset) tilesetManager = TilesetManager.instance() tilesetManager.addReference(tileset) self.tilesetAdded.emit(index, tileset) ## # Removes the tileset at the given \a index from this map. Emits the # appropriate signal. # # \warning Does not make sure that any references to tiles in the removed # tileset are cleared. ## def removeTilesetAt(self, index): self.tilesetAboutToBeRemoved.emit(index) tileset = self.mMap.tilesets().at(index) if (tileset == self.mCurrentObject or isFromTileset(self.mCurrentObject, tileset)): self.setCurrentObject(None) self.mMap.removeTilesetAt(index) self.tilesetRemoved.emit(tileset) tilesetManager = TilesetManager.instance() tilesetManager.removeReference(tileset) def moveTileset(self, _from, to): if (_from == to): return tileset = self.mMap.tilesets().at(_from) self.mMap.removeTilesetAt(_from) self.mMap.insertTileset(to, tileset) self.tilesetMoved.emit(_from, to) def setTilesetFileName(self, tileset, fileName): tileset.setFileName(fileName) self.tilesetFileNameChanged.emit(tileset) def setTilesetName(self, tileset, name): tileset.setName(name) self.tilesetNameChanged.emit(tileset) def setTilesetTileOffset(self, tileset, tileOffset): tileset.setTileOffset(tileOffset) self.mMap.recomputeDrawMargins() self.tilesetTileOffsetChanged.emit(tileset) def duplicateObjects(self, objects): if (objects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Duplicate %n Object(s)", "", objects.size())) clones = QList() for mapObject in objects: clone = mapObject.clone() clones.append(clone) self.mUndoStack.push(AddMapObject(self, mapObject.objectGroup(), clone)) self.mUndoStack.endMacro() self.setSelectedObjects(clones) def removeObjects(self, objects): if (objects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Remove %n Object(s)", "", objects.size())) for mapObject in objects: self.mUndoStack.push(RemoveMapObject(self, mapObject)) self.mUndoStack.endMacro() def moveObjectsToGroup(self, objects, objectGroup): if (objects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Move %n Object(s) to Layer", "", objects.size())) for mapObject in objects: if (mapObject.objectGroup() == objectGroup): continue self.mUndoStack.push(MoveMapObjectToGroup(self, mapObject, objectGroup)) self.mUndoStack.endMacro() def setProperty(self, object, name, value): hadProperty = object.hasProperty(name) object.setProperty(name, value) if (hadProperty): self.propertyChanged.emit(object, name) else: self.propertyAdded.emit(object, name) def setProperties(self, object, properties): object.setProperties(properties) self.propertiesChanged.emit(object) def removeProperty(self, object, name): object.removeProperty(name) self.propertyRemoved.emit(object, name) ## # Returns the layer model. Can be used to modify the layer stack of the # map, and to display the layer stack in a view. ## def layerModel(self): return self.mLayerModel def mapObjectModel(self): return self.mMapObjectModel def terrainModel(self): return self.mTerrainModel ## # Returns the map renderer. ## def renderer(self): return self.mRenderer ## # Creates the map renderer. Should be called after changing the map # orientation. ## def createRenderer(self): if (self.mRenderer): del self.mRenderer x = self.mMap.orientation() if x==Map.Orientation.Isometric: self.mRenderer = IsometricRenderer(self.mMap) elif x==Map.Orientation.Staggered: self.mRenderer = StaggeredRenderer(self.mMap) elif x==Map.Orientation.Hexagonal: self.mRenderer = HexagonalRenderer(self.mMap) else: self.mRenderer = OrthogonalRenderer(self.mMap) ## # Returns the undo stack of this map document. Should be used to push any # commands on that modify the map. ## def undoStack(self): return self.mUndoStack ## # Returns the selected area of tiles. ## def selectedArea(self): return QRegion(self.mSelectedArea) ## # Sets the selected area of tiles. ## def setSelectedArea(self, selection): if (self.mSelectedArea != selection): oldSelectedArea = self.mSelectedArea self.mSelectedArea = selection self.selectedAreaChanged.emit(self.mSelectedArea, oldSelectedArea) ## # Returns the list of selected objects. ## def selectedObjects(self): return self.mSelectedObjects ## # Sets the list of selected objects, emitting the selectedObjectsChanged # signal. ## def setSelectedObjects(self, selectedObjects): if selectedObjects.nequal(self.mSelectedObjects): self.mSelectedObjects = selectedObjects self.selectedObjectsChanged.emit() if (selectedObjects.size() == 1): self.setCurrentObject(selectedObjects.first()) ## # Returns the list of selected tiles. ## def selectedTiles(self): return self.mSelectedTiles def setSelectedTiles(self, selectedTiles): self.mSelectedTiles = selectedTiles self.selectedTilesChanged.emit() def currentObject(self): return self.mCurrentObject def setCurrentObject(self, object): if (object == self.mCurrentObject): return self.mCurrentObject = object self.currentObjectChanged.emit([object]) def currentObjects(self): objects = QList() if (self.mCurrentObject): if (self.mCurrentObject.typeId() == Object.MapObjectType and not self.mSelectedObjects.isEmpty()): for mapObj in self.mSelectedObjects: objects.append(mapObj) elif (self.mCurrentObject.typeId() == Object.TileType and not self.mSelectedTiles.isEmpty()): for tile in self.mSelectedTiles: objects.append(tile) else: objects.append(self.mCurrentObject) return objects def unifyTilesets(self, *args): l = len(args) if l==1: ## # Makes sure the all tilesets which are used at the given \a map will be # present in the map document. # # To reach the aim, all similar tilesets will be replaced by the version # in the current map document and all missing tilesets will be added to # the current map document. # # \warning This method assumes that the tilesets in \a map are managed by # the TilesetManager! ## map = args[0] undoCommands = QList() existingTilesets = self.mMap.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of this map for tileset in map.tilesets(): if (existingTilesets.contains(tileset)): continue replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): undoCommands.append(AddTileset(self, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoCommands.append(ChangeProperties(self, self.tr("Tile"), replacementTile, properties)) map.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) if (not undoCommands.isEmpty()): self.mUndoStack.beginMacro(self.tr("Tileset Changes")) for command in undoCommands: self.mUndoStack.push(command) self.mUndoStack.endMacro() elif l==2: map, missingTilesets = args existingTilesets = self.mMap.tilesets() tilesetManager = TilesetManager.instance() for tileset in map.tilesets(): # tileset already added if existingTilesets.contains(tileset): continue replacement = tileset.findSimilarTileset(existingTilesets) # tileset not present and no replacement tileset found if not replacement: if not missingTilesets.contains(tileset): missingTilesets.append(tileset) continue # replacement tileset found, change given map map.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) ## # Emits the map changed signal. This signal should be emitted after changing # the map size or its tile size. ## def emitMapChanged(self): self.mapChanged.emit() ## # Emits the region changed signal for the specified region. The region # should be in tile coordinates. This method is used by the TilePainter. ## def emitRegionChanged(self, region, layer): self.regionChanged.emit(region, layer) ## # Emits the region edited signal for the specified region and tile layer. # The region should be in tile coordinates. This should be called from # all map document changing classes which are triggered by user input. ## def emitRegionEdited(self, region, layer): self.regionEdited.emit(region, layer) def emitTileLayerDrawMarginsChanged(self, layer): self.tileLayerDrawMarginsChanged.emit(layer) ## # Emits the tileset changed signal. This signal is currently used when adding # or removing tiles from a tileset. # # @todo Emit more specific signals. ## def emitTilesetChanged(self, tileset): self.tilesetChanged.emit(tileset) ## # Emits the signal notifying about the terrain probability of a tile changing. ## def emitTileProbabilityChanged(self, tile): self.tileProbabilityChanged.emit(tile) ## # Emits the signal notifying tileset models about changes to tile terrain # information. All the \a tiles need to be from the same tileset. ## def emitTileTerrainChanged(self, tiles): if (not tiles.isEmpty()): self.tileTerrainChanged.emit(tiles) ## # Emits the signal notifying the TileCollisionEditor about the object group # of a tile changing. ## def emitTileObjectGroupChanged(self, tile): self.tileObjectGroupChanged.emit(tile) ## # Emits the signal notifying about the animation of a tile changing. ## def emitTileAnimationChanged(self, tile): self.tileAnimationChanged.emit(tile) ## # Emits the objectGroupChanged signal, should be called when changing the # color or drawing order of an object group. ## def emitObjectGroupChanged(self, objectGroup): self.objectGroupChanged.emit(objectGroup) ## # Emits the imageLayerChanged signal, should be called when changing the # image or the transparent color of an image layer. ## def emitImageLayerChanged(self, imageLayer): self.imageLayerChanged.emit(imageLayer) ## # Emits the editLayerNameRequested signal, to get renamed. ## def emitEditLayerNameRequested(self): self.editLayerNameRequested.emit() ## # Emits the editCurrentObject signal, which makes the Properties window become # visible and take focus. ## def emitEditCurrentObject(self): self.editCurrentObject.emit() ## # Before forwarding the signal, the objects are removed from the list of # selected objects, triggering a selectedObjectsChanged signal when # appropriate. ## def onObjectsRemoved(self, objects): self.deselectObjects(objects) self.objectsRemoved.emit(objects) def onMapObjectModelRowsInserted(self, parent, first, last): objectGroup = self.mMapObjectModel.toObjectGroup(parent) if (not objectGroup): # we're not dealing with insertion of objects return self.objectsInserted.emit(objectGroup, first, last) self.onMapObjectModelRowsInsertedOrRemoved(parent, first, last) def onMapObjectModelRowsInsertedOrRemoved(self, parent, first, last): objectGroup = self.mMapObjectModel.toObjectGroup(parent) if (not objectGroup): return # Inserting or removing objects changes the index of any that come after lastIndex = objectGroup.objectCount() - 1 if (last < lastIndex): self.objectsIndexChanged.emit(objectGroup, last + 1, lastIndex) def onObjectsMoved(self, parent, start, end, destination, row): if (parent != destination): return objectGroup = self.mMapObjectModel.toObjectGroup(parent) # Determine the full range over which object indexes changed first = min(start, row) last = max(end, row - 1) self.objectsIndexChanged.emit(objectGroup, first, last) def onLayerAdded(self, index): self.layerAdded.emit(index) # Select the first layer that gets added to the map if (self.mMap.layerCount() == 1): self.setCurrentLayerIndex(0) def onLayerAboutToBeRemoved(self, index): layer = self.mMap.layerAt(index) if (layer == self.mCurrentObject): self.setCurrentObject(None) # Deselect any objects on this layer when necessary og = layer if type(og) == ObjectGroup: self.deselectObjects(og.objects()) self.layerAboutToBeRemoved.emit(index) def onLayerRemoved(self, index): # Bring the current layer index to safety currentLayerRemoved = self.mCurrentLayerIndex == self.mMap.layerCount() if (currentLayerRemoved): self.mCurrentLayerIndex = self.mCurrentLayerIndex - 1 self.layerRemoved.emit(index) # Emitted after the layerRemoved signal so that the MapScene has a chance # of synchronizing before adapting to the newly selected index if (currentLayerRemoved): self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex) def onTerrainRemoved(self, terrain): if (terrain == self.mCurrentObject): self.setCurrentObject(None) def setFileName(self, fileName): if (self.mFileName == fileName): return oldFileName = self.mFileName self.mFileName = fileName self.fileNameChanged.emit(fileName, oldFileName) def deselectObjects(self, objects): # Unset the current object when it was part of this list of objects if (self.mCurrentObject and self.mCurrentObject.typeId() == Object.MapObjectType): if (objects.contains(self.mCurrentObject)): self.setCurrentObject(None) removedCount = 0 for object in objects: removedCount += self.mSelectedObjects.removeAll(object) if (removedCount > 0): self.selectedObjectsChanged.emit() def disconnect(self): try: super().disconnect() except: pass
class DiagramScene(QGraphicsScene): """ This class implements the main Diagram Scene. """ GridPen = QPen(QColor(80, 80, 80), 0, Qt.SolidLine) GridSize = 20 MinSize = 2000 MaxSize = 1000000 RecentNum = 5 sgnInsertionEnded = pyqtSignal('QGraphicsItem', int) sgnItemAdded = pyqtSignal('QGraphicsItem') sgnModeChanged = pyqtSignal(DiagramMode) sgnItemRemoved = pyqtSignal('QGraphicsItem') sgnUpdated = pyqtSignal() #################################################################################################################### # # # DIAGRAM SCENE IMPLEMENTATION # # # #################################################################################################################### def __init__(self, mainwindow, parent=None): """ Initialize the diagram scene. :type mainwindow: MainWindow :type parent: QWidget """ super().__init__(parent) self.document = File(parent=self) self.guid = GUID(self) self.factory = ItemFactory(self) self.index = ItemIndex(self) self.meta = PredicateMetaIndex(self) self.undostack = QUndoStack(self) self.undostack.setUndoLimit(50) self.validator = OWL2Validator(self) self.mainwindow = mainwindow self.pasteOffsetX = Clipboard.PasteOffsetX self.pasteOffsetY = Clipboard.PasteOffsetY self.mode = DiagramMode.Idle self.modeParam = Item.Undefined self.mouseOverNode = None self.mousePressEdge = None self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = {} connect(self.sgnItemAdded, self.index.add) connect(self.sgnItemRemoved, self.index.remove) #################################################################################################################### # # # EVENTS # # # #################################################################################################################### def dragEnterEvent(self, dragEvent): """ Executed when a dragged element enters the scene area. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragEnterEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dragMoveEvent(self, dragEvent): """ Executed when an element is dragged over the scene. :type dragEvent: QGraphicsSceneDragDropEvent """ super().dragMoveEvent(dragEvent) if dragEvent.mimeData().hasFormat('text/plain'): dragEvent.setDropAction(Qt.CopyAction) dragEvent.accept() else: dragEvent.ignore() def dropEvent(self, dropEvent): """ Executed when a dragged element is dropped on the scene. :type dropEvent: QGraphicsSceneDragDropEvent """ super().dropEvent(dropEvent) if dropEvent.mimeData().hasFormat('text/plain'): item = Item.forValue(dropEvent.mimeData().text()) node = self.factory.create(item=item, scene=self) node.setPos(snap(dropEvent.scenePos(), DiagramScene.GridSize, self.mainwindow.snapToGrid)) self.undostack.push(CommandNodeAdd(scene=self, node=node)) self.sgnInsertionEnded.emit(node, dropEvent.modifiers()) dropEvent.setDropAction(Qt.CopyAction) dropEvent.accept() else: dropEvent.ignore() def mousePressEvent(self, mouseEvent): """ Executed when a mouse button is clicked on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & Qt.LeftButton: if self.mode is DiagramMode.InsertNode: ######################################################################################################## # # # NODE INSERTION # # # ######################################################################################################## item = Item.forValue(self.modeParam) node = self.factory.create(item, self) node.setPos(snap(mousePos, DiagramScene.GridSize, self.mainwindow.snapToGrid)) self.undostack.push(CommandNodeAdd(self, node)) self.sgnInsertionEnded.emit(node, mouseEvent.modifiers()) super().mousePressEvent(mouseEvent) elif self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## node = self.itemOnTopOf(mousePos, edges=False) if node: item = Item.forValue(self.modeParam) edge = self.factory.create(item, self, source=node) edge.updateEdge(mousePos) self.mousePressEdge = edge self.addItem(edge) super().mousePressEvent(mouseEvent) else: super().mousePressEvent(mouseEvent) if self.mode is DiagramMode.Idle: #################################################################################################### # # # ITEM SELECTION # # # #################################################################################################### # See if we have some nodes selected in the scene: this is needed because itemOnTopOf # will discard labels, so if we have a node whose label is overlapping the node shape, # clicking on the label will make itemOnTopOf return the node item instead of the label. selected = self.selectedNodes() if selected: # We have some nodes selected in the scene so we probably are going to do a # move operation, prepare data for mouse move event => select a node that will act # as mouse grabber to compute delta movements for each componenet in the selection. self.mousePressNode = self.itemOnTopOf(mousePos, edges=False) if self.mousePressNode: self.mousePressNodePos = self.mousePressNode.pos() self.mousePressPos = mousePos self.mousePressData = { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in selected}, 'edges': {} } # Figure out if the nodes we are moving are sharing edges: if so, move the edge # together with the nodes (which actually means moving the edge breakpoints). for node in self.mousePressData['nodes']: for edge in node.edges: if edge not in self.mousePressData['edges']: if edge.other(node).isSelected(): self.mousePressData['edges'][edge] = edge.breakpoints[:] def mouseMoveEvent(self, mouseEvent): """ Executed when then mouse is moved on the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButtons = mouseEvent.buttons() mousePos = mouseEvent.scenePos() if mouseButtons & Qt.LeftButton: if self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## if self.mousePressEdge: edge = self.mousePressEdge edge.updateEdge(mousePos) currentNode = self.itemOnTopOf(mousePos, edges=False, skip={edge.source}) previousNode = self.mouseOverNode statusBar = self.mainwindow.statusBar() if previousNode: previousNode.updateBrush(selected=False) if currentNode: self.mouseOverNode = currentNode res = self.validator.result(edge.source, edge, currentNode) currentNode.updateBrush(selected=False, valid=res.valid) if not res.valid: statusBar.showMessage(res.message) else: statusBar.clearMessage() else: statusBar.clearMessage() self.mouseOverNode = None self.validator.clear() else: if self.mode is DiagramMode.Idle: if self.mousePressNode: self.setMode(DiagramMode.MoveNode) if self.mode is DiagramMode.MoveNode: #################################################################################################### # # # ITEM MOVEMENT # # # #################################################################################################### point = self.mousePressNodePos + mousePos - self.mousePressPos point = snap(point, DiagramScene.GridSize, self.mainwindow.snapToGrid) delta = point - self.mousePressNodePos edges = set() # Update all the breakpoints positions. for edge, breakpoints in self.mousePressData['edges'].items(): for i in range(len(breakpoints)): edge.breakpoints[i] = breakpoints[i] + delta # Move all the selected nodes. for node, data in self.mousePressData['nodes'].items(): edges |= set(node.edges) node.setPos(data['pos'] + delta) for edge, pos in data['anchors'].items(): node.setAnchor(edge, pos + delta) # Update edges. for edge in edges: edge.updateEdge() super().mouseMoveEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): """ Executed when the mouse is released from the scene. :type mouseEvent: QGraphicsSceneMouseEvent """ mouseButton = mouseEvent.button() mousePos = mouseEvent.scenePos() if mouseButton == Qt.LeftButton: if self.mode is DiagramMode.InsertEdge: ######################################################################################################## # # # EDGE INSERTION # # # ######################################################################################################## if self.mousePressEdge: edge = self.mousePressEdge edge.source.updateBrush(selected=False) currentNode = self.itemOnTopOf(mousePos, edges=False, skip={edge.source}) insertEdge = False if currentNode: currentNode.updateBrush(selected=False) if self.validator.valid(edge.source, edge, currentNode): edge.target = currentNode insertEdge = True # We remove the item temporarily from the graphics scene and we perform the add using # the undo command that will also emit the sgnItemAdded signal hence all the widgets will # be notified of the edge insertion. We do this because while creating the edge we need # to display it so the users knows what is he connecting, but we don't want to truly insert # it till it's necessary (when the mouse is released and the validator allows the insertion) self.removeItem(edge) if insertEdge: self.undostack.push(CommandEdgeAdd(self, edge)) edge.updateEdge() self.mouseOverNode = None self.mousePressEdge = None self.clearSelection() self.validator.clear() statusBar = self.mainwindow.statusBar() statusBar.clearMessage() self.sgnInsertionEnded.emit(edge, mouseEvent.modifiers()) elif self.mode is DiagramMode.MoveNode: ######################################################################################################## # # # ITEM MOVEMENT # # # ######################################################################################################## data = { 'undo': self.mousePressData, 'redo': { 'nodes': { node: { 'anchors': {k: v for k, v in node.anchors.items()}, 'pos': node.pos(), } for node in self.mousePressData['nodes']}, 'edges': {x: x.breakpoints[:] for x in self.mousePressData['edges']} } } self.undostack.push(CommandNodeMove(self, data)) self.setMode(DiagramMode.Idle) elif mouseButton == Qt.RightButton: if self.mode is not DiagramMode.SceneDrag: ######################################################################################################## # # # CONTEXT MENU # # # ######################################################################################################## item = self.itemOnTopOf(mousePos) if item: self.clearSelection() item.setSelected(True) self.mousePressPos = mousePos menu = self.mainwindow.menuFactory.create(self.mainwindow, self, item, mousePos) menu.exec_(mouseEvent.screenPos()) super().mouseReleaseEvent(mouseEvent) self.mousePressPos = None self.mousePressNode = None self.mousePressNodePos = None self.mousePressData = None #################################################################################################################### # # # AXIOMS COMPOSITION # # # #################################################################################################################### def propertyAxiomComposition(self, source, restriction): """ Returns a collection of items to be added to the given source node to compose a property axiom. :type source: AbstractNode :type restriction: class :rtype: set """ node = restriction(scene=self) edge = InputEdge(scene=self, source=source, target=node) size = DiagramScene.GridSize offsets = ( QPointF(snapF(+source.width() / 2 + 70, size), 0), QPointF(snapF(-source.width() / 2 - 70, size), 0), QPointF(0, snapF(-source.height() / 2 - 70, size)), QPointF(0, snapF(+source.height() / 2 + 70, size)), QPointF(snapF(+source.width() / 2 + 70, size), snapF(-source.height() / 2 - 70, size)), QPointF(snapF(-source.width() / 2 - 70, size), snapF(-source.height() / 2 - 70, size)), QPointF(snapF(+source.width() / 2 + 70, size), snapF(+source.height() / 2 + 70, size)), QPointF(snapF(-source.width() / 2 - 70, size), snapF(+source.height() / 2 + 70, size)), ) pos = None num = sys.maxsize rad = QPointF(node.width() / 2, node.height() / 2) for o in offsets: count = len(self.items(QRectF(source.pos() + o - rad, source.pos() + o + rad))) if count < num: num = count pos = source.pos() + o node.setPos(pos) return {node, edge} def propertyDomainAxiomComposition(self, source): """ Returns a collection of items to be added to the given source node to compose a property domain. :type source: AbstractNode :rtype: set """ return self.propertyAxiomComposition(source, DomainRestrictionNode) def propertyRangeAxiomComposition(self, source): """ Returns a collection of items to be added to the given source node to compose a property range. :type source: AbstractNode :rtype: set """ return self.propertyAxiomComposition(source, RangeRestrictionNode) #################################################################################################################### # # # SLOTS # # # #################################################################################################################### @pyqtSlot() def clear(self): """ Clear the diagram by removing all the elements. """ self.index.clear() self.undostack.clear() super().clear() #################################################################################################################### # # # INTERFACE # # # #################################################################################################################### def edge(self, eid): """ Returns the edge matching the given edge id. :type eid: str """ return self.index.edgeForId(eid) def edges(self): """ Returns a view on all the edges of the diagram. :rtype: view """ return self.index.edges() def itemOnTopOf(self, point, nodes=True, edges=True, skip=None): """ Returns the shape which is on top of the given point. :type point: QPointF :type nodes: bool :type edges: bool :type skip: iterable :rtype: Item """ skip = skip or {} data = [x for x in self.items(point) if (nodes and x.node or edges and x.edge) and x not in skip] if data: return max(data, key=lambda x: x.zValue()) return None def node(self, nid): """ Returns the node matching the given node id. :type nid: str """ return self.index.nodeForId(nid) def nodes(self): """ Returns a view on all the nodes in the diagram. :rtype: view """ return self.index.nodes() def selectedEdges(self): """ Returns the edges selected in the scene. :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.edge] def selectedItems(self): """ Returns the items selected in the scene (will filter out labels since we don't need them). :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.node or x.edge] def selectedNodes(self): """ Returns the nodes selected in the scene. :rtype: list """ return [x for x in super(DiagramScene, self).selectedItems() if x.node] def setMode(self, mode, param=None): """ Set the operation mode. :type mode: DiagramMode :type param: int """ if self.mode != mode or self.modeParam != param: self.mode = mode self.modeParam = param self.sgnModeChanged.emit(mode) def visibleRect(self, margin=0): """ Returns a rectangle matching the area of visible items. :type margin: float :rtype: QRectF """ bound = self.itemsBoundingRect() topLeft = QPointF(bound.left() - margin, bound.top() - margin) bottomRight = QPointF(bound.right() + margin, bound.bottom() + margin) return QRectF(topLeft, bottomRight)
class IconEditorGrid(QWidget): """ Class implementing the icon editor grid. @signal canRedoChanged(bool) emitted after the redo status has changed @signal canUndoChanged(bool) emitted after the undo status has changed @signal clipboardImageAvailable(bool) emitted to signal the availability of an image to be pasted @signal colorChanged(QColor) emitted after the drawing color was changed @signal imageChanged(bool) emitted after the image was modified @signal positionChanged(int, int) emitted after the cursor poition was changed @signal previewChanged(QPixmap) emitted to signal a new preview pixmap @signal selectionAvailable(bool) emitted to signal a change of the selection @signal sizeChanged(int, int) emitted after the size has been changed @signal zoomChanged(int) emitted to signal a change of the zoom value """ canRedoChanged = pyqtSignal(bool) canUndoChanged = pyqtSignal(bool) clipboardImageAvailable = pyqtSignal(bool) colorChanged = pyqtSignal(QColor) imageChanged = pyqtSignal(bool) positionChanged = pyqtSignal(int, int) previewChanged = pyqtSignal(QPixmap) selectionAvailable = pyqtSignal(bool) sizeChanged = pyqtSignal(int, int) zoomChanged = pyqtSignal(int) Pencil = 1 Rubber = 2 Line = 3 Rectangle = 4 FilledRectangle = 5 Circle = 6 FilledCircle = 7 Ellipse = 8 FilledEllipse = 9 Fill = 10 ColorPicker = 11 RectangleSelection = 20 CircleSelection = 21 MarkColor = QColor(255, 255, 255, 255) NoMarkColor = QColor(0, 0, 0, 0) ZoomMinimum = 100 ZoomMaximum = 10000 ZoomStep = 100 ZoomDefault = 1200 ZoomPercent = True def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (QWidget) """ super(IconEditorGrid, self).__init__(parent) self.setAttribute(Qt.WA_StaticContents) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.__curColor = Qt.black self.__zoom = 12 self.__curTool = self.Pencil self.__startPos = QPoint() self.__endPos = QPoint() self.__dirty = False self.__selecting = False self.__selRect = QRect() self.__isPasting = False self.__clipboardSize = QSize() self.__pasteRect = QRect() self.__undoStack = QUndoStack(self) self.__currentUndoCmd = None self.__image = QImage(32, 32, QImage.Format_ARGB32) self.__image.fill(qRgba(0, 0, 0, 0)) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) self.__compositingMode = QPainter.CompositionMode_SourceOver self.__lastPos = (-1, -1) self.__gridEnabled = True self.__selectionAvailable = False self.__initCursors() self.__initUndoTexts() self.setMouseTracking(True) self.__undoStack.canRedoChanged.connect(self.canRedoChanged) self.__undoStack.canUndoChanged.connect(self.canUndoChanged) self.__undoStack.cleanChanged.connect(self.__cleanChanged) self.imageChanged.connect(self.__updatePreviewPixmap) QApplication.clipboard().dataChanged.connect(self.__checkClipboard) self.__checkClipboard() def __initCursors(self): """ Private method to initialize the various cursors. """ self.__normalCursor = QCursor(Qt.ArrowCursor) pix = QPixmap(":colorpicker-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__colorPickerCursor = QCursor(pix, 1, 21) pix = QPixmap(":paintbrush-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__paintCursor = QCursor(pix, 0, 19) pix = QPixmap(":fill-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__fillCursor = QCursor(pix, 3, 20) pix = QPixmap(":aim-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__aimCursor = QCursor(pix, 10, 10) pix = QPixmap(":eraser-cursor.xpm") mask = pix.createHeuristicMask() pix.setMask(mask) self.__rubberCursor = QCursor(pix, 1, 16) def __initUndoTexts(self): """ Private method to initialize texts to be associated with undo commands for the various drawing tools. """ self.__undoTexts = { self.Pencil: self.tr("Set Pixel"), self.Rubber: self.tr("Erase Pixel"), self.Line: self.tr("Draw Line"), self.Rectangle: self.tr("Draw Rectangle"), self.FilledRectangle: self.tr("Draw Filled Rectangle"), self.Circle: self.tr("Draw Circle"), self.FilledCircle: self.tr("Draw Filled Circle"), self.Ellipse: self.tr("Draw Ellipse"), self.FilledEllipse: self.tr("Draw Filled Ellipse"), self.Fill: self.tr("Fill Region"), } def isDirty(self): """ Public method to check the dirty status. @return flag indicating a modified status (boolean) """ return self.__dirty def setDirty(self, dirty, setCleanState=False): """ Public slot to set the dirty flag. @param dirty flag indicating the new modification status (boolean) @param setCleanState flag indicating to set the undo stack to clean (boolean) """ self.__dirty = dirty self.imageChanged.emit(dirty) if not dirty and setCleanState: self.__undoStack.setClean() def sizeHint(self): """ Public method to report the size hint. @return size hint (QSize) """ size = self.__zoom * self.__image.size() if self.__zoom >= 3 and self.__gridEnabled: size += QSize(1, 1) return size def setPenColor(self, newColor): """ Public method to set the drawing color. @param newColor reference to the new color (QColor) """ self.__curColor = QColor(newColor) self.colorChanged.emit(QColor(newColor)) def penColor(self): """ Public method to get the current drawing color. @return current drawing color (QColor) """ return QColor(self.__curColor) def setCompositingMode(self, mode): """ Public method to set the compositing mode. @param mode compositing mode to set (QPainter.CompositionMode) """ self.__compositingMode = mode def compositingMode(self): """ Public method to get the compositing mode. @return compositing mode (QPainter.CompositionMode) """ return self.__compositingMode def setTool(self, tool): """ Public method to set the current drawing tool. @param tool drawing tool to be used (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection) """ self.__curTool = tool self.__lastPos = (-1, -1) if self.__curTool in [self.RectangleSelection, self.CircleSelection]: self.__selecting = True else: self.__selecting = False if self.__curTool in [self.RectangleSelection, self.CircleSelection, self.Line, self.Rectangle, self.FilledRectangle, self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse]: self.setCursor(self.__aimCursor) elif self.__curTool == self.Fill: self.setCursor(self.__fillCursor) elif self.__curTool == self.ColorPicker: self.setCursor(self.__colorPickerCursor) elif self.__curTool == self.Pencil: self.setCursor(self.__paintCursor) elif self.__curTool == self.Rubber: self.setCursor(self.__rubberCursor) else: self.setCursor(self.__normalCursor) def tool(self): """ Public method to get the current drawing tool. @return current drawing tool (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection) """ return self.__curTool def setIconImage(self, newImage, undoRedo=False, clearUndo=False): """ Public method to set a new icon image. @param newImage reference to the new image (QImage) @keyparam undoRedo flag indicating an undo or redo operation (boolean) @keyparam clearUndo flag indicating to clear the undo stack (boolean) """ if newImage != self.__image: self.__image = newImage.convertToFormat(QImage.Format_ARGB32) self.update() self.updateGeometry() self.resize(self.sizeHint()) self.__markImage = QImage(self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) if undoRedo: self.setDirty(not self.__undoStack.isClean()) else: self.setDirty(False) if clearUndo: self.__undoStack.clear() self.sizeChanged.emit(*self.iconSize()) def iconImage(self): """ Public method to get a copy of the icon image. @return copy of the icon image (QImage) """ return QImage(self.__image) def iconSize(self): """ Public method to get the size of the icon. @return width and height of the image as a tuple (integer, integer) """ return self.__image.width(), self.__image.height() def setZoomFactor(self, newZoom): """ Public method to set the zoom factor in percent. @param newZoom zoom factor (integer >= 100) """ newZoom = max(100, newZoom) # must not be less than 100 if newZoom != self.__zoom: self.__zoom = newZoom // 100 self.update() self.updateGeometry() self.resize(self.sizeHint()) self.zoomChanged.emit(int(self.__zoom * 100)) def zoomFactor(self): """ Public method to get the current zoom factor in percent. @return zoom factor (integer) """ return self.__zoom * 100 def setGridEnabled(self, enable): """ Public method to enable the display of grid lines. @param enable enabled status of the grid lines (boolean) """ if enable != self.__gridEnabled: self.__gridEnabled = enable self.update() def isGridEnabled(self): """ Public method to get the grid lines status. @return enabled status of the grid lines (boolean) """ return self.__gridEnabled def paintEvent(self, evt): """ Protected method called to repaint some of the widget. @param evt reference to the paint event object (QPaintEvent) """ painter = QPainter(self) if self.__zoom >= 3 and self.__gridEnabled: painter.setPen(self.palette().windowText().color()) i = 0 while i <= self.__image.width(): painter.drawLine( self.__zoom * i, 0, self.__zoom * i, self.__zoom * self.__image.height()) i += 1 j = 0 while j <= self.__image.height(): painter.drawLine( 0, self.__zoom * j, self.__zoom * self.__image.width(), self.__zoom * j) j += 1 col = QColor("#aaa") painter.setPen(Qt.DashLine) for i in range(0, self.__image.width()): for j in range(0, self.__image.height()): rect = self.__pixelRect(i, j) if evt.region().intersects(rect): color = QColor.fromRgba(self.__image.pixel(i, j)) painter.fillRect(rect, QBrush(Qt.white)) painter.fillRect(QRect(rect.topLeft(), rect.center()), col) painter.fillRect(QRect(rect.center(), rect.bottomRight()), col) painter.fillRect(rect, QBrush(color)) if self.__isMarked(i, j): painter.drawRect(rect.adjusted(0, 0, -1, -1)) painter.end() def __pixelRect(self, i, j): """ Private method to determine the rectangle for a given pixel coordinate. @param i x-coordinate of the pixel in the image (integer) @param j y-coordinate of the pixel in the image (integer) @return rectangle for the given pixel coordinates (QRect) """ if self.__zoom >= 3 and self.__gridEnabled: return QRect(self.__zoom * i + 1, self.__zoom * j + 1, self.__zoom - 1, self.__zoom - 1) else: return QRect(self.__zoom * i, self.__zoom * j, self.__zoom, self.__zoom) def mousePressEvent(self, evt): """ Protected method to handle mouse button press events. @param evt reference to the mouse event object (QMouseEvent) """ if evt.button() == Qt.LeftButton: if self.__isPasting: self.__isPasting = False self.editPaste(True) self.__markImage.fill(self.NoMarkColor.rgba()) self.update(self.__pasteRect) self.__pasteRect = QRect() return if self.__curTool == self.Pencil: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__setImagePixel(evt.pos(), True) self.setDirty(True) self.__undoStack.push(cmd) self.__currentUndoCmd = cmd elif self.__curTool == self.Rubber: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__setImagePixel(evt.pos(), False) self.setDirty(True) self.__undoStack.push(cmd) self.__currentUndoCmd = cmd elif self.__curTool == self.Fill: i, j = self.__imageCoordinates(evt.pos()) col = QColor() col.setRgba(self.__image.pixel(i, j)) cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) self.__drawFlood(i, j, col) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) elif self.__curTool == self.ColorPicker: i, j = self.__imageCoordinates(evt.pos()) col = QColor() col.setRgba(self.__image.pixel(i, j)) self.setPenColor(col) else: self.__unMark() self.__startPos = evt.pos() self.__endPos = evt.pos() def mouseMoveEvent(self, evt): """ Protected method to handle mouse move events. @param evt reference to the mouse event object (QMouseEvent) """ self.positionChanged.emit(*self.__imageCoordinates(evt.pos())) if self.__isPasting and not (evt.buttons() & Qt.LeftButton): self.__drawPasteRect(evt.pos()) return if evt.buttons() & Qt.LeftButton: if self.__curTool == self.Pencil: self.__setImagePixel(evt.pos(), True) self.setDirty(True) elif self.__curTool == self.Rubber: self.__setImagePixel(evt.pos(), False) self.setDirty(True) elif self.__curTool in [self.Fill, self.ColorPicker]: pass # do nothing else: self.__drawTool(evt.pos(), True) def mouseReleaseEvent(self, evt): """ Protected method to handle mouse button release events. @param evt reference to the mouse event object (QMouseEvent) """ if evt.button() == Qt.LeftButton: if self.__curTool in [self.Pencil, self.Rubber]: if self.__currentUndoCmd: self.__currentUndoCmd.setAfterImage(self.__image) self.__currentUndoCmd = None if self.__curTool not in [self.Pencil, self.Rubber, self.Fill, self.ColorPicker, self.RectangleSelection, self.CircleSelection]: cmd = IconEditCommand(self, self.__undoTexts[self.__curTool], self.__image) if self.__drawTool(evt.pos(), False): self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.setDirty(True) def __setImagePixel(self, pos, opaque): """ Private slot to set or erase a pixel. @param pos position of the pixel in the widget (QPoint) @param opaque flag indicating a set operation (boolean) """ i, j = self.__imageCoordinates(pos) if self.__image.rect().contains(i, j) and (i, j) != self.__lastPos: if opaque: painter = QPainter(self.__image) painter.setPen(self.penColor()) painter.setCompositionMode(self.__compositingMode) painter.drawPoint(i, j) else: self.__image.setPixel(i, j, qRgba(0, 0, 0, 0)) self.__lastPos = (i, j) self.update(self.__pixelRect(i, j)) def __imageCoordinates(self, pos): """ Private method to convert from widget to image coordinates. @param pos widget coordinate (QPoint) @return tuple with the image coordinates (tuple of two integers) """ i = pos.x() // self.__zoom j = pos.y() // self.__zoom return i, j def __drawPasteRect(self, pos): """ Private slot to draw a rectangle for signaling a paste operation. @param pos widget position of the paste rectangle (QPoint) """ self.__markImage.fill(self.NoMarkColor.rgba()) if self.__pasteRect.isValid(): self.__updateImageRect( self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) x, y = self.__imageCoordinates(pos) isize = self.__image.size() if x + self.__clipboardSize.width() <= isize.width(): sx = self.__clipboardSize.width() else: sx = isize.width() - x if y + self.__clipboardSize.height() <= isize.height(): sy = self.__clipboardSize.height() else: sy = isize.height() - y self.__pasteRect = QRect(QPoint(x, y), QSize(sx - 1, sy - 1)) painter = QPainter(self.__markImage) painter.setPen(self.MarkColor) painter.drawRect(self.__pasteRect) painter.end() self.__updateImageRect(self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) def __drawTool(self, pos, mark): """ Private method to perform a draw operation depending of the current tool. @param pos widget coordinate to perform the draw operation at (QPoint) @param mark flag indicating a mark operation (boolean) @return flag indicating a successful draw (boolean) """ self.__unMark() if mark: self.__endPos = QPoint(pos) drawColor = self.MarkColor img = self.__markImage else: drawColor = self.penColor() img = self.__image start = QPoint(*self.__imageCoordinates(self.__startPos)) end = QPoint(*self.__imageCoordinates(pos)) painter = QPainter(img) painter.setPen(drawColor) painter.setCompositionMode(self.__compositingMode) if self.__curTool == self.Line: painter.drawLine(start, end) elif self.__curTool in [self.Rectangle, self.FilledRectangle, self.RectangleSelection]: left = min(start.x(), end.x()) top = min(start.y(), end.y()) right = max(start.x(), end.x()) bottom = max(start.y(), end.y()) if self.__curTool == self.RectangleSelection: painter.setBrush(QBrush(drawColor)) if self.__curTool == self.FilledRectangle: for y in range(top, bottom + 1): painter.drawLine(left, y, right, y) else: painter.drawRect(left, top, right - left, bottom - top) if self.__selecting: self.__selRect = QRect( left, top, right - left + 1, bottom - top + 1) self.__selectionAvailable = True self.selectionAvailable.emit(True) elif self.__curTool in [self.Circle, self.FilledCircle, self.CircleSelection]: r = max(abs(start.x() - end.x()), abs(start.y() - end.y())) if self.__curTool in [self.FilledCircle, self.CircleSelection]: painter.setBrush(QBrush(drawColor)) painter.drawEllipse(start, r, r) if self.__selecting: self.__selRect = QRect(start.x() - r, start.y() - r, 2 * r + 1, 2 * r + 1) self.__selectionAvailable = True self.selectionAvailable.emit(True) elif self.__curTool in [self.Ellipse, self.FilledEllipse]: r1 = abs(start.x() - end.x()) r2 = abs(start.y() - end.y()) if r1 == 0 or r2 == 0: return False if self.__curTool == self.FilledEllipse: painter.setBrush(QBrush(drawColor)) painter.drawEllipse(start, r1, r2) painter.end() if self.__curTool in [self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse]: self.update() else: self.__updateRect(self.__startPos, pos) return True def __drawFlood(self, i, j, oldColor, doUpdate=True): """ Private method to perform a flood fill operation. @param i x-value in image coordinates (integer) @param j y-value in image coordinates (integer) @param oldColor reference to the color at position i, j (QColor) @param doUpdate flag indicating an update is requested (boolean) (used for speed optimizations) """ if not self.__image.rect().contains(i, j) or \ self.__image.pixel(i, j) != oldColor.rgba() or \ self.__image.pixel(i, j) == self.penColor().rgba(): return self.__image.setPixel(i, j, self.penColor().rgba()) self.__drawFlood(i, j - 1, oldColor, False) self.__drawFlood(i, j + 1, oldColor, False) self.__drawFlood(i - 1, j, oldColor, False) self.__drawFlood(i + 1, j, oldColor, False) if doUpdate: self.update() def __updateRect(self, pos1, pos2): """ Private slot to update parts of the widget. @param pos1 top, left position for the update in widget coordinates (QPoint) @param pos2 bottom, right position for the update in widget coordinates (QPoint) """ self.__updateImageRect(QPoint(*self.__imageCoordinates(pos1)), QPoint(*self.__imageCoordinates(pos2))) def __updateImageRect(self, ipos1, ipos2): """ Private slot to update parts of the widget. @param ipos1 top, left position for the update in image coordinates (QPoint) @param ipos2 bottom, right position for the update in image coordinates (QPoint) """ r1 = self.__pixelRect(ipos1.x(), ipos1.y()) r2 = self.__pixelRect(ipos2.x(), ipos2.y()) left = min(r1.x(), r2.x()) top = min(r1.y(), r2.y()) right = max(r1.x() + r1.width(), r2.x() + r2.width()) bottom = max(r1.y() + r1.height(), r2.y() + r2.height()) self.update(left, top, right - left + 1, bottom - top + 1) def __unMark(self): """ Private slot to remove the mark indicator. """ self.__markImage.fill(self.NoMarkColor.rgba()) if self.__curTool in [self.Circle, self.FilledCircle, self.Ellipse, self.FilledEllipse, self.CircleSelection]: self.update() else: self.__updateRect(self.__startPos, self.__endPos) if self.__selecting: self.__selRect = QRect() self.__selectionAvailable = False self.selectionAvailable.emit(False) def __isMarked(self, i, j): """ Private method to check, if a pixel is marked. @param i x-value in image coordinates (integer) @param j y-value in image coordinates (integer) @return flag indicating a marked pixel (boolean) """ return self.__markImage.pixel(i, j) == self.MarkColor.rgba() def __updatePreviewPixmap(self): """ Private slot to generate and signal an updated preview pixmap. """ p = QPixmap.fromImage(self.__image) self.previewChanged.emit(p) def previewPixmap(self): """ Public method to generate a preview pixmap. @return preview pixmap (QPixmap) """ p = QPixmap.fromImage(self.__image) return p def __checkClipboard(self): """ Private slot to check, if the clipboard contains a valid image, and signal the result. """ ok = self.__clipboardImage()[1] self.__clipboardImageAvailable = ok self.clipboardImageAvailable.emit(ok) def canPaste(self): """ Public slot to check the availability of the paste operation. @return flag indicating availability of paste (boolean) """ return self.__clipboardImageAvailable def __clipboardImage(self): """ Private method to get an image from the clipboard. @return tuple with the image (QImage) and a flag indicating a valid image (boolean) """ img = QApplication.clipboard().image() ok = not img.isNull() if ok: img = img.convertToFormat(QImage.Format_ARGB32) return img, ok def __getSelectionImage(self, cut): """ Private method to get an image from the selection. @param cut flag indicating to cut the selection (boolean) @return image of the selection (QImage) """ if cut: cmd = IconEditCommand(self, self.tr("Cut Selection"), self.__image) img = QImage(self.__selRect.size(), QImage.Format_ARGB32) img.fill(qRgba(0, 0, 0, 0)) for i in range(0, self.__selRect.width()): for j in range(0, self.__selRect.height()): if self.__image.rect().contains(self.__selRect.x() + i, self.__selRect.y() + j): if self.__isMarked( self.__selRect.x() + i, self.__selRect.y() + j): img.setPixel(i, j, self.__image.pixel( self.__selRect.x() + i, self.__selRect.y() + j)) if cut: self.__image.setPixel(self.__selRect.x() + i, self.__selRect.y() + j, qRgba(0, 0, 0, 0)) if cut: self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.__unMark() if cut: self.update(self.__selRect) return img def editCopy(self): """ Public slot to copy the selection. """ if self.__selRect.isValid(): img = self.__getSelectionImage(False) QApplication.clipboard().setImage(img) def editCut(self): """ Public slot to cut the selection. """ if self.__selRect.isValid(): img = self.__getSelectionImage(True) QApplication.clipboard().setImage(img) @pyqtSlot() def editPaste(self, pasting=False): """ Public slot to paste an image from the clipboard. @param pasting flag indicating part two of the paste operation (boolean) """ img, ok = self.__clipboardImage() if ok: if img.width() > self.__image.width() or \ img.height() > self.__image.height(): res = E5MessageBox.yesNo( self, self.tr("Paste"), self.tr( """<p>The clipboard image is larger than the""" """ current image.<br/>Paste as new image?</p>""")) if res: self.editPasteAsNew() return elif not pasting: self.__isPasting = True self.__clipboardSize = img.size() else: cmd = IconEditCommand(self, self.tr("Paste Clipboard"), self.__image) self.__markImage.fill(self.NoMarkColor.rgba()) painter = QPainter(self.__image) painter.setPen(self.penColor()) painter.setCompositionMode(self.__compositingMode) painter.drawImage( self.__pasteRect.x(), self.__pasteRect.y(), img, 0, 0, self.__pasteRect.width() + 1, self.__pasteRect.height() + 1) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) self.__updateImageRect( self.__pasteRect.topLeft(), self.__pasteRect.bottomRight() + QPoint(1, 1)) else: E5MessageBox.warning( self, self.tr("Pasting Image"), self.tr("""Invalid image data in clipboard.""")) def editPasteAsNew(self): """ Public slot to paste the clipboard as a new image. """ img, ok = self.__clipboardImage() if ok: cmd = IconEditCommand( self, self.tr("Paste Clipboard as New Image"), self.__image) self.setIconImage(img) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editSelectAll(self): """ Public slot to select the complete image. """ self.__unMark() self.__startPos = QPoint(0, 0) self.__endPos = QPoint(self.rect().bottomRight()) self.__markImage.fill(self.MarkColor.rgba()) self.__selRect = self.__image.rect() self.__selectionAvailable = True self.selectionAvailable.emit(True) self.update() def editClear(self): """ Public slot to clear the image. """ self.__unMark() cmd = IconEditCommand(self, self.tr("Clear Image"), self.__image) self.__image.fill(qRgba(0, 0, 0, 0)) self.update() self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editResize(self): """ Public slot to resize the image. """ from .IconSizeDialog import IconSizeDialog dlg = IconSizeDialog(self.__image.width(), self.__image.height()) res = dlg.exec_() if res == QDialog.Accepted: newWidth, newHeight = dlg.getData() if newWidth != self.__image.width() or \ newHeight != self.__image.height(): cmd = IconEditCommand(self, self.tr("Resize Image"), self.__image) img = self.__image.scaled( newWidth, newHeight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) self.setIconImage(img) self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editNew(self): """ Public slot to generate a new, empty image. """ from .IconSizeDialog import IconSizeDialog dlg = IconSizeDialog(self.__image.width(), self.__image.height()) res = dlg.exec_() if res == QDialog.Accepted: width, height = dlg.getData() img = QImage(width, height, QImage.Format_ARGB32) img.fill(qRgba(0, 0, 0, 0)) self.setIconImage(img) def grayScale(self): """ Public slot to convert the image to gray preserving transparency. """ cmd = IconEditCommand(self, self.tr("Convert to Grayscale"), self.__image) for x in range(self.__image.width()): for y in range(self.__image.height()): col = self.__image.pixel(x, y) if col != qRgba(0, 0, 0, 0): gray = qGray(col) self.__image.setPixel( x, y, qRgba(gray, gray, gray, qAlpha(col))) self.update() self.setDirty(True) self.__undoStack.push(cmd) cmd.setAfterImage(self.__image) def editUndo(self): """ Public slot to perform an undo operation. """ if self.__undoStack.canUndo(): self.__undoStack.undo() def editRedo(self): """ Public slot to perform a redo operation. """ if self.__undoStack.canRedo(): self.__undoStack.redo() def canUndo(self): """ Public method to return the undo status. @return flag indicating the availability of undo (boolean) """ return self.__undoStack.canUndo() def canRedo(self): """ Public method to return the redo status. @return flag indicating the availability of redo (boolean) """ return self.__undoStack.canRedo() def __cleanChanged(self, clean): """ Private slot to handle the undo stack clean state change. @param clean flag indicating the clean state (boolean) """ self.setDirty(not clean) def shutdown(self): """ Public slot to perform some shutdown actions. """ self.__undoStack.canRedoChanged.disconnect(self.canRedoChanged) self.__undoStack.canUndoChanged.disconnect(self.canUndoChanged) self.__undoStack.cleanChanged.disconnect(self.__cleanChanged) def isSelectionAvailable(self): """ Public method to check the availability of a selection. @return flag indicating the availability of a selection (boolean) """ return self.__selectionAvailable
class BookmarksManager(QObject): """ Class implementing the bookmarks manager. @signal entryAdded(BookmarkNode) emitted after a bookmark node has been added @signal entryRemoved(BookmarkNode, int, BookmarkNode) emitted after a bookmark node has been removed @signal entryChanged(BookmarkNode) emitted after a bookmark node has been changed @signal bookmarksSaved() emitted after the bookmarks were saved @signal bookmarksReloaded() emitted after the bookmarks were reloaded """ entryAdded = pyqtSignal(BookmarkNode) entryRemoved = pyqtSignal(BookmarkNode, int, BookmarkNode) entryChanged = pyqtSignal(BookmarkNode) bookmarksSaved = pyqtSignal() bookmarksReloaded = pyqtSignal() def __init__(self, parent=None): """ Constructor @param parent reference to the parent object (QObject) """ super(BookmarksManager, self).__init__(parent) self.__saveTimer = AutoSaver(self, self.save) self.entryAdded.connect(self.__saveTimer.changeOccurred) self.entryRemoved.connect(self.__saveTimer.changeOccurred) self.entryChanged.connect(self.__saveTimer.changeOccurred) self.__initialize() def __initialize(self): """ Private method to initialize some data. """ self.__loaded = False self.__bookmarkRootNode = None self.__toolbar = None self.__menu = None self.__bookmarksModel = None self.__commands = QUndoStack() @classmethod def getFileName(cls): """ Class method to get the file name of the bookmark file. @return name of the bookmark file (string) """ return os.path.join(Utilities.getConfigDir(), "browser", "bookmarks.xbel") def close(self): """ Public method to close the bookmark manager. """ self.__saveTimer.saveIfNeccessary() def undoRedoStack(self): """ Public method to get a reference to the undo stack. @return reference to the undo stack (QUndoStack) """ return self.__commands def changeExpanded(self): """ Public method to handle a change of the expanded state. """ self.__saveTimer.changeOccurred() def reload(self): """ Public method used to initiate a reloading of the bookmarks. """ self.__initialize() self.load() self.bookmarksReloaded.emit() def load(self): """ Public method to load the bookmarks. @exception RuntimeError raised to indicate an error loading the bookmarks """ if self.__loaded: return self.__loaded = True bookmarkFile = self.getFileName() if not QFile.exists(bookmarkFile): from . import DefaultBookmarks_rc # __IGNORE_WARNING__ bookmarkFile = QFile(":/DefaultBookmarks.xbel") bookmarkFile.open(QIODevice.ReadOnly) from .XbelReader import XbelReader reader = XbelReader() self.__bookmarkRootNode = reader.read(bookmarkFile) if reader.error() != QXmlStreamReader.NoError: E5MessageBox.warning( None, self.tr("Loading Bookmarks"), self.tr( """Error when loading bookmarks on line {0},""" """ column {1}:\n {2}""") .format(reader.lineNumber(), reader.columnNumber(), reader.errorString())) others = [] for index in range( len(self.__bookmarkRootNode.children()) - 1, -1, -1): node = self.__bookmarkRootNode.children()[index] if node.type() == BookmarkNode.Folder: if (node.title == self.tr("Toolbar Bookmarks") or node.title == BOOKMARKBAR) and \ self.__toolbar is None: node.title = self.tr(BOOKMARKBAR) self.__toolbar = node if (node.title == self.tr("Menu") or node.title == BOOKMARKMENU) and \ self.__menu is None: node.title = self.tr(BOOKMARKMENU) self.__menu = node else: others.append(node) self.__bookmarkRootNode.remove(node) if len(self.__bookmarkRootNode.children()) > 0: raise RuntimeError("Error loading bookmarks.") if self.__toolbar is None: self.__toolbar = BookmarkNode(BookmarkNode.Folder, self.__bookmarkRootNode) self.__toolbar.title = self.tr(BOOKMARKBAR) else: self.__bookmarkRootNode.add(self.__toolbar) if self.__menu is None: self.__menu = BookmarkNode(BookmarkNode.Folder, self.__bookmarkRootNode) self.__menu.title = self.tr(BOOKMARKMENU) else: self.__bookmarkRootNode.add(self.__menu) for node in others: self.__menu.add(node) self.__convertFromOldBookmarks() def save(self): """ Public method to save the bookmarks. """ if not self.__loaded: return from .XbelWriter import XbelWriter writer = XbelWriter() bookmarkFile = self.getFileName() # save root folder titles in English (i.e. not localized) self.__menu.title = BOOKMARKMENU self.__toolbar.title = BOOKMARKBAR if not writer.write(bookmarkFile, self.__bookmarkRootNode): E5MessageBox.warning( None, self.tr("Saving Bookmarks"), self.tr("""Error saving bookmarks to <b>{0}</b>.""") .format(bookmarkFile)) # restore localized titles self.__menu.title = self.tr(BOOKMARKMENU) self.__toolbar.title = self.tr(BOOKMARKBAR) self.bookmarksSaved.emit() def addBookmark(self, parent, node, row=-1): """ Public method to add a bookmark. @param parent reference to the node to add to (BookmarkNode) @param node reference to the node to add (BookmarkNode) @param row row number (integer) """ if not self.__loaded: return self.setTimestamp(node, BookmarkNode.TsAdded, QDateTime.currentDateTime()) command = InsertBookmarksCommand(self, parent, node, row) self.__commands.push(command) def removeBookmark(self, node): """ Public method to remove a bookmark. @param node reference to the node to be removed (BookmarkNode) """ if not self.__loaded: return parent = node.parent() row = parent.children().index(node) command = RemoveBookmarksCommand(self, parent, row) self.__commands.push(command) def setTitle(self, node, newTitle): """ Public method to set the title of a bookmark. @param node reference to the node to be changed (BookmarkNode) @param newTitle title to be set (string) """ if not self.__loaded: return command = ChangeBookmarkCommand(self, node, newTitle, True) self.__commands.push(command) def setUrl(self, node, newUrl): """ Public method to set the URL of a bookmark. @param node reference to the node to be changed (BookmarkNode) @param newUrl URL to be set (string) """ if not self.__loaded: return command = ChangeBookmarkCommand(self, node, newUrl, False) self.__commands.push(command) def setNodeChanged(self, node): """ Public method to signal changes of bookmarks other than title, URL or timestamp. @param node reference to the bookmark (BookmarkNode) """ self.__saveTimer.changeOccurred() def setTimestamp(self, node, timestampType, timestamp): """ Public method to set the URL of a bookmark. @param node reference to the node to be changed (BookmarkNode) @param timestampType type of the timestamp to set (BookmarkNode.TsAdded, BookmarkNode.TsModified, BookmarkNode.TsVisited) @param timestamp timestamp to set (QDateTime) """ if not self.__loaded: return assert timestampType in [BookmarkNode.TsAdded, BookmarkNode.TsModified, BookmarkNode.TsVisited] if timestampType == BookmarkNode.TsAdded: node.added = timestamp elif timestampType == BookmarkNode.TsModified: node.modified = timestamp elif timestampType == BookmarkNode.TsVisited: node.visited = timestamp self.__saveTimer.changeOccurred() def bookmarks(self): """ Public method to get a reference to the root bookmark node. @return reference to the root bookmark node (BookmarkNode) """ if not self.__loaded: self.load() return self.__bookmarkRootNode def menu(self): """ Public method to get a reference to the bookmarks menu node. @return reference to the bookmarks menu node (BookmarkNode) """ if not self.__loaded: self.load() return self.__menu def toolbar(self): """ Public method to get a reference to the bookmarks toolbar node. @return reference to the bookmarks toolbar node (BookmarkNode) """ if not self.__loaded: self.load() return self.__toolbar def bookmarksModel(self): """ Public method to get a reference to the bookmarks model. @return reference to the bookmarks model (BookmarksModel) """ if self.__bookmarksModel is None: from .BookmarksModel import BookmarksModel self.__bookmarksModel = BookmarksModel(self, self) return self.__bookmarksModel def importBookmarks(self): """ Public method to import bookmarks. """ from .BookmarksImportDialog import BookmarksImportDialog dlg = BookmarksImportDialog() if dlg.exec_() == QDialog.Accepted: importRootNode = dlg.getImportedBookmarks() if importRootNode is not None: self.addBookmark(self.menu(), importRootNode) def exportBookmarks(self): """ Public method to export the bookmarks. """ fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( None, self.tr("Export Bookmarks"), "eric6_bookmarks.xbel", self.tr("XBEL bookmarks (*.xbel);;" "XBEL bookmarks (*.xml);;" "HTML Bookmarks (*.html)")) if not fileName: return ext = QFileInfo(fileName).suffix() if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fileName += ex ext = QFileInfo(fileName).suffix() if ext == "html": from .NsHtmlWriter import NsHtmlWriter writer = NsHtmlWriter() else: from .XbelWriter import XbelWriter writer = XbelWriter() if not writer.write(fileName, self.__bookmarkRootNode): E5MessageBox.critical( None, self.tr("Exporting Bookmarks"), self.tr("""Error exporting bookmarks to <b>{0}</b>.""") .format(fileName)) def __convertFromOldBookmarks(self): """ Private method to convert the old bookmarks into the new ones. """ bmNames = Preferences.Prefs.settings.value('Bookmarks/Names') bmFiles = Preferences.Prefs.settings.value('Bookmarks/Files') if bmNames is not None and bmFiles is not None: if len(bmNames) == len(bmFiles): convertedRootNode = BookmarkNode(BookmarkNode.Folder) convertedRootNode.title = self.tr("Converted {0}")\ .format(QDate.currentDate().toString( Qt.SystemLocaleShortDate)) for i in range(len(bmNames)): node = BookmarkNode(BookmarkNode.Bookmark, convertedRootNode) node.title = bmNames[i] url = QUrl(bmFiles[i]) if not url.scheme(): url.setScheme("file") node.url = url.toString() self.addBookmark(self.menu(), convertedRootNode) Preferences.Prefs.settings.remove('Bookmarks') def iconChanged(self, url): """ Public slot to update the icon image for an URL. @param url URL of the icon to update (QUrl or string) """ if isinstance(url, QUrl): url = url.toString() nodes = self.bookmarksForUrl(url) for node in nodes: self.bookmarksModel().entryChanged(node) def bookmarkForUrl(self, url, start=StartRoot): """ Public method to get a bookmark node for a given URL. @param url URL of the bookmark to search for (QUrl or string) @keyparam start indicator for the start of the search (StartRoot, StartMenu, StartToolBar) @return bookmark node for the given url (BookmarkNode) """ if start == StartMenu: startNode = self.__menu elif start == StartToolBar: startNode = self.__toolbar else: startNode = self.__bookmarkRootNode if startNode is None: return None if isinstance(url, QUrl): url = url.toString() return self.__searchBookmark(url, startNode) def __searchBookmark(self, url, startNode): """ Private method get a bookmark node for a given URL. @param url URL of the bookmark to search for (string) @param startNode reference to the node to start searching (BookmarkNode) @return bookmark node for the given url (BookmarkNode) """ bm = None for node in startNode.children(): if node.type() == BookmarkNode.Folder: bm = self.__searchBookmark(url, node) elif node.type() == BookmarkNode.Bookmark: if node.url == url: bm = node if bm is not None: return bm return None def bookmarksForUrl(self, url, start=StartRoot): """ Public method to get a list of bookmark nodes for a given URL. @param url URL of the bookmarks to search for (QUrl or string) @keyparam start indicator for the start of the search (StartRoot, StartMenu, StartToolBar) @return list of bookmark nodes for the given url (list of BookmarkNode) """ if start == StartMenu: startNode = self.__menu elif start == StartToolBar: startNode = self.__toolbar else: startNode = self.__bookmarkRootNode if startNode is None: return None if isinstance(url, QUrl): url = url.toString() return self.__searchBookmarks(url, startNode) def __searchBookmarks(self, url, startNode): """ Private method get a list of bookmark nodes for a given URL. @param url URL of the bookmarks to search for (string) @param startNode reference to the node to start searching (BookmarkNode) @return list of bookmark nodes for the given url (list of BookmarkNode) """ bm = [] for node in startNode.children(): if node.type() == BookmarkNode.Folder: bm.extend(self.__searchBookmarks(url, node)) elif node.type() == BookmarkNode.Bookmark: if node.url == url: bm.append(node) return bm