def __init__(self, path, **kwargs): super().__init__(**kwargs) self.path = path parent = os.path.abspath(os.path.join(path, pardir)) if not isdir(parent): log.info('Creating directory', path=parent) os.mkdir(parent) if not isfile(self.path): log.info('Creating file', path=self.path) with open(self.path, 'w') as f: f.write('# Auto-Generated Configuration File' + os.linesep + os.linesep) f.write(f'# Node Launcher version {NODE_LAUNCHER_RELEASE}' + os.linesep + os.linesep) f.flush() self.cache = {} self.aliases = { 'rpcport': 'main.rpcport', 'main.rpcport': 'rpcport', 'port': 'main.port', 'main.port': 'port', 'walletdir': 'main.walletdir', 'main.walletdir': 'walletdir' } self.populate_cache() self.file_watcher = QFileSystemWatcher() self.file_watcher.addPath(self.path)
def __init__(self, toolbox, name, description, references, x, y): """Class constructor.""" super().__init__(name, description) self._toolbox = toolbox self._project = self._toolbox.project() self.item_type = "Data Connection" # self._widget = DataConnectionWidget(self, self.item_type) self.reference_model = QStandardItemModel() # References to files self.data_model = QStandardItemModel( ) # Paths of project internal files. These are found in DC data directory self.datapackage_icon = QIcon(QPixmap(":/icons/datapkg.png")) self.data_dir_watcher = QFileSystemWatcher(self) # Make project directory for this Data Connection self.data_dir = os.path.join(self._project.project_dir, self.short_name) try: create_dir(self.data_dir) self.data_dir_watcher.addPath(self.data_dir) except OSError: self._toolbox.msg_error.emit( "[OSError] Creating directory {0} failed." " Check permissions.".format(self.data_dir)) # Populate references model self.references = references self.populate_reference_list(self.references) # Populate data (files) model data_files = self.data_files() self.populate_data_list(data_files) self._graphics_item = DataConnectionImage(self._toolbox, x - 35, y - 35, 70, 70, self.name) self.spine_datapackage_form = None # self.ui.toolButton_datapackage.setMenu(self.datapackage_popup_menu) # TODO: OBSOLETE? self._sigs = self.make_signal_handler_dict()
def __init__(self): super(DownloadPostProcess, self).__init__(None) self.file_watcher = QFileSystemWatcher() self.file_watcher.fileChanged.connect(self.read_bytes) self.logger = create_logger(__name__)
def open_file(self, path): self.file_watcher = QFileSystemWatcher() self.file_watcher.addPath(path) self.file_watcher.fileChanged.connect(self.reload) save_setting('file_path', path) persist_storage() self.path = path self.reload(path)
def __init__(self, image_path): super(ImageView, self).__init__(None) self.scene = QGraphicsScene() self.setScene(self.scene) self.pixmapitem = self.scene.addPixmap(QPixmap.fromImage(QImage(image_path))) self.last_release_time = 0 self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self.refresh_image) # Register file watcher self.watcher.addPath(image_path)
def __init__(self, datapackage): """Initialize class. Args: datapackage (CustomPackage): Data package associated to this widget """ from ..ui.spine_datapackage_form import Ui_MainWindow # pylint: disable=import-outside-toplevel super().__init__(flags=Qt.Window) self.datapackage = datapackage self.selected_resource_index = None self.resources_model = DatapackageResourcesModel(self, self.datapackage) self.fields_model = DatapackageFieldsModel(self, self.datapackage) self.foreign_keys_model = DatapackageForeignKeysModel(self, self.datapackage) self.resource_data_model = DatapackageResourceDataModel(self, self.datapackage) self.default_row_height = QFontMetrics(QFont("", 0)).lineSpacing() max_screen_height = max([s.availableSize().height() for s in QGuiApplication.screens()]) self.visible_rows = int(max_screen_height / self.default_row_height) self.err_msg = QErrorMessage(self) self.notification_stack = NotificationStack(self) self._foreign_keys_context_menu = QMenu(self) self._file_watcher = QFileSystemWatcher(self) self._file_watcher.addPath(self.datapackage.base_path) self._changed_source_indexes = set() self.undo_group = QUndoGroup(self) self.undo_stacks = {} self._save_resource_actions = [] self.ui = Ui_MainWindow() self.ui.setupUi(self) self.takeCentralWidget() self._before_save_all = self.ui.menuFile.insertSeparator(self.ui.actionSave_All) self.setWindowIcon(QIcon(":/symbols/app.ico")) self.qsettings = QSettings("SpineProject", "Spine Toolbox") self.restore_ui() self.add_menu_actions() self.setStyleSheet(MAINWINDOW_SS) self.ui.tableView_resources.setModel(self.resources_model) self.ui.tableView_resources.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_resource_data.setModel(self.resource_data_model) self.ui.tableView_resource_data.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_resource_data.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.ui.tableView_fields.setModel(self.fields_model) self.ui.tableView_fields.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_fields.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.ui.tableView_foreign_keys.setModel(self.foreign_keys_model) self.ui.tableView_foreign_keys.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_foreign_keys.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.connect_signals() self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle("{0}[*] - Spine datapackage manager".format(self.datapackage.base_path)) self.load_datapackage()
def __init__(self, image_path): ''' Execution order: parent's __init__ -> self.event() -> rest code in self.__init__ So, we must initialize at here, otherwise it will override value which is assigned in self.event(). ''' self.child_obj = None super(WebView, self).__init__(None) self.load(QUrl(image_path)) self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self.refresh_image) # Register file watcher self.watcher.addPath(image_path) self.setContextMenuPolicy(Qt.NoContextMenu) # Disable right click context menu
def __init__(self, parent: QWidget = None): """ Initializes Model Args: parent(QWidget): Parent widget """ super().__init__(parent) self.file_system_watcher = QFileSystemWatcher() self.dirtied_files = {} self.ignored_substrings = {'.cpython', '__pycache__'} self.is_dev_mode = False self.columns = ['QComponents', 'Rebuild Buttons']
def detect(self) -> None: self.tabs_location = paths.find_file(Firefox.TABS_LOCATION_PATTERN) if self.tabs_location: self.logger.info("Firefox detected={tabs_location}".format( tabs_location=(bool(self.tabs_location != "")))) self.tabs_file_watcher = QFileSystemWatcher() self.get_tabs(self.tabs_location) self.tabs_file_watcher.fileChanged.connect(self.get_tabs, Qt.QueuedConnection) self.detected = True else: self.detected = False
def detect(self) -> None: try: self.tabs_location = subprocess.check_output( Firefox.SESSION_LOCATION_COMMAND, shell=True).decode("utf-8").replace("\n", "") self.logger.info("Firefox detected={tabs_location}".format( tabs_location=(bool(self.tabs_location != "")))) self.tabs_file_watcher = QFileSystemWatcher() self.get_tabs(self.tabs_location) self.tabs_file_watcher.fileChanged.connect(self.get_tabs, Qt.QueuedConnection) self.detected = True except subprocess.CalledProcessError as error: self.detected = False
def __init__(self, name, description, x, y, toolbox, project, logger, references=None): """Data Connection class. Args: name (str): Object name description (str): Object description x (float): Initial X coordinate of item icon y (float): Initial Y coordinate of item icon toolbox (ToolboxUI): QMainWindow instance project (SpineToolboxProject): the project this item belongs to logger (LoggerInterface): a logger instance references (list): a list of file paths """ super().__init__(name, description, x, y, project, logger) self._toolbox = toolbox self.reference_model = QStandardItemModel() # References to files self.data_model = QStandardItemModel( ) # Paths of project internal files. These are found in DC data directory self.datapackage_icon = QIcon(QPixmap(":/icons/datapkg.png")) self.data_dir_watcher = QFileSystemWatcher(self) if os.path.isdir(self.data_dir): self.data_dir_watcher.addPath(self.data_dir) # Populate references model if references is None: references = list() # Convert relative paths to absolute absolute_refs = [ deserialize_path(r, self._project.project_dir) for r in references ] self.references = absolute_refs self.populate_reference_list(self.references) # Populate data (files) model data_files = self.data_files() self.populate_data_list(data_files) self.spine_datapackage_form = None self.data_dir_watcher.directoryChanged.connect(self.refresh)
def __init__(self, sourceFile="", watching=True, verbose=False, parent=None): """ watching -- Defines whether the watcher is active (default: True) verbose -- if True, output log infos (default: False) """ super(QmlInstantEngine, self).__init__(parent) self._fileWatcher = QFileSystemWatcher() # Internal Qt File Watcher self._sourceFile = "" self._watchedFiles = [] # Internal watched files list self._verbose = verbose # Verbose bool self._watching = False # self._extensions = [ "qml", "js" ] # File extensions that defines files to watch when adding a folder self._rootItem = None def onObjectCreated(root, url): if not root: return # Restore root item geometry if self._rootItem: root.setGeometry(self._rootItem.geometry()) self._rootItem.deleteLater() self._rootItem = root self.objectCreated.connect(onObjectCreated) # Update the watching status self.setWatching(watching) if sourceFile: self.load(sourceFile)
def __init__(self, parent=None): super(Loader, self).__init__(parent=parent) self.ui = Ui_Loader() self.ui.setupUi(self) self.dir = QDir(QDir.currentPath() + '/programs/') self.dir.setFilter(QDir.Files or QDir.NoDotAndDotDot) self.fs_watcher = QFileSystemWatcher(self.dir.path()) self.fs_watcher.addPath(self.dir.path()) self.fs_watcher.directoryChanged.connect(self.update_program_list) self.send_status = QProgressDialog self.sender = Sender self.serialpropertiesvalues = \ { 'baudrate': Serial.BAUDRATES, 'parity': Serial.PARITIES, 'databits': Serial.BYTESIZES, 'stopbits': Serial.STOPBITS, 'flowcontrol': ['NoControl', 'SoftwareControl', 'HardwareControl'] } self.update_program_list() self.update_serial_port_list() # self.set_serial_port_options() self.ui.updateProgramListButton.clicked.connect(self.refresh) self.ui.programListWidget.itemSelectionChanged.connect( self.selection_changed) self.ui.sendButton.clicked.connect(self.send_program) self.ui.serialPortChooser.currentTextChanged.connect( self.selection_changed) self.ui.serialPortChooser.currentTextChanged.connect(save_port) # self.ui.baudRateInput.textChanged.connect(save_baud) # self.ui.parityChooser.currentTextChanged.connect(save_parity) # self.ui.dataBitsChooser.currentTextChanged.connect(save_databits) # self.ui.stopBitsChooser.currentTextChanged.connect(save_stopbits) # self.ui.flowControlChooser.currentTextChanged.connect(save_flowcontrol) self.thread_pool = QThreadPool()
class DownloadPostProcess(QObject): bytes_processed = Signal(int) started = Signal() def __init__(self): super(DownloadPostProcess, self).__init__(None) self.file_watcher = QFileSystemWatcher() self.file_watcher.fileChanged.connect(self.read_bytes) self.logger = create_logger(__name__) @Slot(str) def track(self, url: str) -> None: track_success = self.file_watcher.addPath(url) self.logger.info("Track {file} success={success}".format( file=url, success=track_success)) self.started.emit() @Slot(str) def read_bytes(self, path: str) -> None: bytes = QFileInfo(path).size() self.bytes_processed.emit(bytes) self.logger.debug("Read {bytes} bytes".format(bytes=bytes))
class ImageView(QGraphicsView): # Tell PageWidget that a file is dropped onto view. dropped_relay = Signal(QDropEvent) def __init__(self, image_path): super(ImageView, self).__init__(None) self.scene = QGraphicsScene() self.setScene(self.scene) self.pixmapitem = self.scene.addPixmap(QPixmap.fromImage(QImage(image_path))) self.last_release_time = 0 self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self.refresh_image) # Register file watcher self.watcher.addPath(image_path) def dragEnterEvent(self, drag_enter_event): # QDragEnterEvent if drag_enter_event.mimeData().hasUrls(): drag_enter_event.acceptProposedAction() # https://stackoverflow.com/a/4421835/4112667 def dragMoveEvent(self, event): pass def dropEvent(self, drop_event): # QDropEvent self.dropped_relay.emit(drop_event) ''' When overwriting an image file, I guess Windows will delete it and then create a new file with the same name. So this function will be called twice. The first round is triggered by deleting. In this case, the image file doesn't exist, so QImage and QPixmap are all invalid and as a result, the view will become white background. Only after the image being created and the function is called for the second time, will the view show the image normally. The User will notice a white flicker because of two rounds of callings. To resolve this problem, we need to detect the invalid QImage or QPixmap and skip the unintended round. ''' def refresh_image(self, image_path): qimage = QImage(image_path) if qimage.isNull(): return pixmap = QPixmap.fromImage(qimage) self.scene.removeItem(self.pixmapitem) self.pixmapitem = self.scene.addPixmap(pixmap) # This will make scrollbar fit the image self.setSceneRect(QRectF(pixmap.rect())) def mousePressEvent(self, mouse_event): # QMouseEvent if mouse_event.button() == Qt.LeftButton: self.setDragMode(QGraphicsView.ScrollHandDrag) elif mouse_event.button() == Qt.RightButton: self.setDragMode(QGraphicsView.RubberBandDrag) QGraphicsView.mousePressEvent(self, mouse_event) def mouseReleaseEvent(self, mouse_event): # QMouseEvent QGraphicsView.mouseReleaseEvent(self, mouse_event) if mouse_event.button() == Qt.LeftButton: self.setDragMode(QGraphicsView.NoDrag) elif mouse_event.button() == Qt.RightButton: self.setDragMode(QGraphicsView.NoDrag) now = time.time() delta = now - self.last_release_time self.last_release_time = now if delta < 0.3: # fast double click self.resetTransform() # Reset to original size (reset scale matrix) return # Maybe a selection selection = self.scene.selectionArea().boundingRect() self.scene.setSelectionArea(QPainterPath()) if selection.isValid(): self.fitInView(selection, Qt.KeepAspectRatio) def wheelEvent(self, wheel_event): # QWheelEvent num_degrees = wheel_event.angleDelta().y() / 8 num_steps = num_degrees / 15 coefficient = 1 + (num_steps * 0.25) self.scale(coefficient, coefficient)
def __init__(self): super().__init__() self.setAttribute( Qt.WA_DeleteOnClose ) # let Qt delete stuff before the python garbage-collector gets to work self.repo = None self.branches_model = None # instantiate main window self.ui = ui.Ui_MainWindow() self.ui.setupUi(self) self.fs_watch = QFileSystemWatcher(self) self.fs_watch.fileChanged.connect(self.on_file_changed) self.fs_watch.directoryChanged.connect(self.on_dir_changed) self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'pqgit', 'config') # for comparison self.new_c_id, self.old_c_id = None, None # window icon cwd = os.path.dirname(os.path.realpath(__file__)) self.setWindowIcon(QIcon(os.path.join(cwd, 'Git-Icon-White.png'))) self.setWindowTitle('pqgit') # size and position self.move(self.settings.value('w/pos', QPoint(200, 200))) self.resize(self.settings.value('w/size', QSize(1000, 1000))) self.ui.hist_splitter.setSizes([ int(s) for s in self.settings.value('w/hist_splitter', [720, 360]) ]) self.ui.cinf_splitter.setSizes([ int(s) for s in self.settings.value('w/cinf_splitter', [360, 360]) ]) self.ui.diff_splitter.setSizes([ int(s) for s in self.settings.value('w/diff_splitter', [150, 1200, 230]) ]) # open repo dir open_shortcut = QShortcut(QKeySequence('Ctrl+O'), self) open_shortcut.activated.connect(self.open_dir) # set-up ui self.branches_model = BranchesModel() self.ui.tvBranches.setModel(self.branches_model) self.ui.tvBranches.selectionModel().selectionChanged.connect( self.branches_selection_changed) self.ui.tvBranches.resizeColumnsToContents() self.history_model = HistoryModel() self.ui.tvHistory.setModel(self.history_model) self.ui.tvHistory.selectionModel().selectionChanged.connect( self.history_selection_changed) self.files_model = FilesModel() self.ui.tvFiles.setModel(self.files_model) self.ui.tvFiles.selectionModel().selectionChanged.connect( self.files_selection_changed) self.ui.tvFiles.doubleClicked.connect(self.on_file_doubleclicked) for view in (self.ui.tvBranches, self.ui.tvHistory, self.ui.tvFiles): view.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) view.setSelectionBehavior(QAbstractItemView.SelectRows) view.setShowGrid(False) view.verticalHeader().setDefaultSectionSize( QApplication.font().pointSize() + 2) view.verticalHeader().hide() self.ui.teDiff.setFont(QFont('Monospace')) self.difftools = [] timer = QTimer(self) timer.timeout.connect(self.on_timer) timer.start(5000) self.dir_name = self.settings.value('last_opened_repo', None) try: pygit2.Repository(self.dir_name) except Exception: #pylint: disable=broad-except self.open_dir() return self.open_repo()
class ConfigurationFile(dict): file_watcher: QFileSystemWatcher def __init__(self, path, **kwargs): super().__init__(**kwargs) self.path = path parent = os.path.abspath(os.path.join(path, pardir)) if not isdir(parent): log.info('Creating directory', path=parent) os.mkdir(parent) if not isfile(self.path): log.info('Creating file', path=self.path) with open(self.path, 'w') as f: f.write('# Auto-Generated Configuration File' + os.linesep + os.linesep) f.write(f'# Node Launcher version {NODE_LAUNCHER_RELEASE}' + os.linesep + os.linesep) f.flush() self.cache = {} self.aliases = { 'rpcport': 'main.rpcport', 'main.rpcport': 'rpcport', 'port': 'main.port', 'main.port': 'port', 'walletdir': 'main.walletdir', 'main.walletdir': 'walletdir' } self.populate_cache() self.file_watcher = QFileSystemWatcher() self.file_watcher.addPath(self.path) def populate_cache(self): with open(self.path, 'r') as f: property_lines = f.readlines() self.cache = {} for property_line in property_lines: key_value = property_line.split('=') key = key_value[0] if not key.strip(): continue value = key_value[1:] value = '='.join(value).strip() value = value.replace('"', '') if len(value) == 1 and value.isdigit(): value = bool(int(value)) elif value.isdigit(): value = int(value) existing_value = self.cache.get(key, 'no_key') if existing_value == 'no_key': self.cache[key] = value if key in self.aliases.keys(): self.cache[self.aliases[key]] = value elif isinstance(existing_value, list): self.cache[key].append(value) if key in self.aliases.keys(): self.cache[self.aliases[key]].append(value) else: self.cache[key] = [existing_value, value] if key in self.aliases.keys(): self.cache[self.aliases[key]] = [existing_value, value] def __repr__(self): return f'ConfigurationFile: {self.path}' def __delitem__(self, v) -> None: raise NotImplementedError() def __len__(self) -> int: raise NotImplementedError() def __iter__(self): raise NotImplementedError() def __getitem__(self, name): if name in self.aliases.keys(): if self.cache.get(self.aliases[name], None) is not None: return self.cache[self.aliases[name]] else: return self.cache.get(name, None) return self.cache.get(name, None) def __setitem__(self, name: str, value: Any) -> None: self.cache[name] = value if name in self.aliases.keys(): self.cache[self.aliases[name]] = value if isinstance(value, str): value = [value] elif isinstance(value, bool): value = [str(int(value))] elif isinstance(value, int): value = [str(value)] elif isinstance(value, List): for item in value: assert isinstance(item, str) pass elif value is None: pass else: raise NotImplementedError(f'setattr for {type(value)}') self.write_property(name, value) def write_property(self, name: str, value_list: List[str]): with open(self.path, 'r') as f: lines = f.readlines() lines = [l.strip() for l in lines if l.strip()] property_lines = [ line_number for line_number, l in enumerate(lines) if l.startswith(name) ] for property_line_index in property_lines: lines.pop(property_line_index) if value_list is not None: for value in value_list: property_string = f'{name.strip()}={value.strip()}' lines.append(property_string) with open(self.path, 'w') as f: lines = [l + os.linesep for l in lines] f.writelines(lines) @property def directory(self): directory_path = os.path.abspath(os.path.join(self.path, os.pardir)) return directory_path @property def snapshot(self): return self.cache.copy()
class MainWindow(QMainWindow): def __init__(self, app, parent=None): super(MainWindow, self).__init__(parent) self.imagesDir = app.dir + '/images/' self.setWindowIcon(QIcon(self.imagesDir + 'icon.png')) self.path = '' self.settings = QSettings() self.lastDir = self.settings.value('lastDir', '') self.setMinimumWidth(540) self.supportedFormats = [] for f in QImageReader.supportedImageFormats(): self.supportedFormats.append(str(f.data(), encoding="utf-8")) self.fileWatcher = QFileSystemWatcher() self.fileWatcher.fileChanged.connect(self.fileChanged) # widgets self.showPixmapWidget = None self.tileWidthSpinBox = QSpinBox() self.tileWidthSpinBox.setValue(16) self.tileWidthSpinBox.setFixedWidth(50) self.tileWidthSpinBox.setMinimum(1) self.tileHeightSpinBox = QSpinBox() self.tileHeightSpinBox.setValue(16) self.tileHeightSpinBox.setFixedWidth(50) self.tileHeightSpinBox.setMinimum(1) self.paddingSpinBox = QSpinBox() self.paddingSpinBox.setFixedWidth(50) self.paddingSpinBox.setMinimum(1) self.transparentCheckbox = QCheckBox("Transparent") self.transparentCheckbox.setChecked(True) self.transparentCheckbox.stateChanged.connect(self.transparentChanged) self.backgroundColorEdit = ColorEdit() self.backgroundColorEdit.setEnabled(False) self.backgroundColorLabel = QLabel("Background color:") self.backgroundColorLabel.setEnabled(False) self.forcePotCheckBox = QCheckBox("Force PoT") self.forcePotCheckBox.setChecked(True) self.forcePotCheckBox.stateChanged.connect(self.forcePotChanged) self.reorderTilesCheckBox = QCheckBox("Reorder tiles") self.generateAndExportButton = QPushButton("Generate and export") self.generateAndExportButton.setFixedHeight(32) self.generateAndExportButton.clicked.connect(self.generateAndExportClicked) self.generateAndExportButton.setEnabled(False) self.pixmapWidget = PixmapWidget() self.pixmapWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.pixmapWidget.setPixmap(self.createDropTextPixmap()) self.pixmapWidget.dropSignal.connect(self.fileDropped) self.pixmapWidget.setMinimumHeight(300) # load settings self.tileWidthSpinBox.setValue(int(self.settings.value('tileWidth', 16))) self.tileHeightSpinBox.setValue(int(self.settings.value('tileHeight', 16))) self.paddingSpinBox.setValue(int(self.settings.value('padding', 1))) self.forcePotCheckBox.setChecked(True if self.settings.value('forcePot', 'true') == 'true' else False) self.reorderTilesCheckBox.setChecked(True if self.settings.value('reorderTiles', 'false') == 'true' else False) self.transparentCheckbox.setChecked(True if self.settings.value('transparent', 'false') == 'true' else False) self.backgroundColorEdit.setColorText(str(self.settings.value('backgroundColor', '#FF00FF'))) self.restoreGeometry(QByteArray(self.settings.value('MainWindow/geometry'))) self.restoreState(QByteArray(self.settings.value('MainWindow/windowState'))) # layout hl1 = QHBoxLayout() hl1.setContentsMargins(5, 5, 5, 5) hl1.addWidget(QLabel("Tile width:")) hl1.addSpacing(5) hl1.addWidget(self.tileWidthSpinBox) hl1.addSpacing(15) hl1.addWidget(QLabel("Tile height:")) hl1.addSpacing(5) hl1.addWidget(self.tileHeightSpinBox) hl1.addSpacing(15) hl1.addWidget(QLabel("Padding:")) hl1.addSpacing(5) hl1.addWidget(self.paddingSpinBox) hl1.addSpacing(15) hl1.addWidget(self.forcePotCheckBox) hl1.addSpacing(15) hl1.addWidget(self.reorderTilesCheckBox) hl1.addStretch() hl2 = QHBoxLayout() hl2.setContentsMargins(5, 5, 5, 5) hl2.addWidget(self.transparentCheckbox) hl2.addSpacing(15) hl2.addWidget(self.backgroundColorLabel) hl2.addSpacing(5) hl2.addWidget(self.backgroundColorEdit) hl2.addStretch() hl3 = QHBoxLayout() hl3.setContentsMargins(5, 5, 5, 5) hl3.addWidget(self.generateAndExportButton) vl = QVBoxLayout() vl.setContentsMargins(0, 0, 0, 0) vl.setSpacing(0) vl.addLayout(hl1) vl.addLayout(hl2) vl.addWidget(self.pixmapWidget) vl.addLayout(hl3) w = QWidget() w.setLayout(vl) self.setCentralWidget(w) self.setTitle() def setTitle(self): p = ' - ' + os.path.basename(self.path) if self.path else '' self.setWindowTitle(QCoreApplication.applicationName() + ' ' + QCoreApplication.applicationVersion() + p) def createDropTextPixmap(self): pixmap = QPixmap(481, 300) pixmap.fill(QColor("#333333")) painter = QPainter(pixmap) font = QFont("Arial") font.setPixelSize(28) font.setBold(True) fm = QFontMetrics(font) painter.setFont(font) painter.setPen(QPen(QColor("#888888"), 1)) text = "Drop the tileset image here" x = (pixmap.width()-fm.width(text))/2 y = (pixmap.height()+fm.height())/2 painter.drawText(x, y, text) del painter return pixmap def fileDropped(self, path): path = str(path) name, ext = os.path.splitext(path) ext = ext[1:] if not ext in self.supportedFormats: QMessageBox.warning(self, "Warning", "The dropped file is not supported") return pixmap = QPixmap(path) if pixmap.isNull(): QMessageBox.warning(self, "Warning", "Can't load the image") return if self.path: self.fileWatcher.removePath(self.path) self.path = path self.fileWatcher.addPath(self.path) self.pixmapWidget.setPixmap(pixmap) self.generateAndExportButton.setEnabled(True) self.setTitle() self.activateWindow() def fileChanged(self, path): #self.fileDropped(path) pass def transparentChanged(self): e = self.transparentCheckbox.isChecked() self.backgroundColorEdit.setEnabled(not e) self.backgroundColorLabel.setEnabled(not e) def forcePotChanged(self): e = self.forcePotCheckBox.isChecked() self.reorderTilesCheckBox.setEnabled(e) def generateAndExportClicked(self): g = Generator() g.tileWidth = self.tileWidthSpinBox.value() g.tileHeight = self.tileHeightSpinBox.value() g.forcePot = self.forcePotCheckBox.isChecked() g.isTransparent = self.transparentCheckbox.isChecked() g.bgColor = self.backgroundColorEdit.getColor() g.reorder = self.reorderTilesCheckBox.isChecked() g.padding = self.paddingSpinBox.value() target = g.create(self.pixmapWidget.pixmap); # export self.lastDir = os.path.dirname(self.path) targetPath = QFileDialog.getSaveFileName(self, 'Export', self.lastDir, 'PNG (*.png)') if targetPath: target.save(targetPath[0]) showPixmap = QPixmap.fromImage(target) if self.showPixmapWidget: self.showPixmapWidget.deleteLater() del self.showPixmapWidget self.showPixmapWidget = PixmapWidget() self.showPixmapWidget.setWindowIcon(self.windowIcon()) self.showPixmapWidget.setWindowTitle(os.path.basename(targetPath[0])) self.showPixmapWidget.resize(showPixmap.width(), showPixmap.height()) self.showPixmapWidget.setPixmap(showPixmap) self.showPixmapWidget.show() def closeEvent(self, event): if self.showPixmapWidget: self.showPixmapWidget.close() # save settings self.settings.setValue('tileWidth', self.tileWidthSpinBox.value()) self.settings.setValue('tileHeight', self.tileHeightSpinBox.value()) self.settings.setValue('padding', self.paddingSpinBox.value()) self.settings.setValue('forcePot', self.forcePotCheckBox.isChecked()) self.settings.setValue('reorderTiles', self.reorderTilesCheckBox.isChecked()) self.settings.setValue('transparent', self.transparentCheckbox.isChecked()) self.settings.setValue('backgroundColor', self.backgroundColorEdit.getColor().name()) self.settings.setValue('lastDir', self.lastDir) self.settings.setValue('MainWindow/geometry', self.saveGeometry()) self.settings.setValue('MainWindow/windowState', self.saveState()) super(MainWindow, self).closeEvent(event)
class SpineDatapackageWidget(QMainWindow): """A widget to edit CSV files in a Data Connection and create a tabular datapackage. """ msg = Signal(str) msg_error = Signal(str) def __init__(self, datapackage): """Initialize class. Args: datapackage (CustomPackage): Data package associated to this widget """ from ..ui.spine_datapackage_form import Ui_MainWindow # pylint: disable=import-outside-toplevel super().__init__(flags=Qt.Window) self.datapackage = datapackage self.selected_resource_index = None self.resources_model = DatapackageResourcesModel(self, self.datapackage) self.fields_model = DatapackageFieldsModel(self, self.datapackage) self.foreign_keys_model = DatapackageForeignKeysModel(self, self.datapackage) self.resource_data_model = DatapackageResourceDataModel(self, self.datapackage) self.default_row_height = QFontMetrics(QFont("", 0)).lineSpacing() max_screen_height = max([s.availableSize().height() for s in QGuiApplication.screens()]) self.visible_rows = int(max_screen_height / self.default_row_height) self.err_msg = QErrorMessage(self) self.notification_stack = NotificationStack(self) self._foreign_keys_context_menu = QMenu(self) self._file_watcher = QFileSystemWatcher(self) self._file_watcher.addPath(self.datapackage.base_path) self._changed_source_indexes = set() self.undo_group = QUndoGroup(self) self.undo_stacks = {} self._save_resource_actions = [] self.ui = Ui_MainWindow() self.ui.setupUi(self) self.takeCentralWidget() self._before_save_all = self.ui.menuFile.insertSeparator(self.ui.actionSave_All) self.setWindowIcon(QIcon(":/symbols/app.ico")) self.qsettings = QSettings("SpineProject", "Spine Toolbox") self.restore_ui() self.add_menu_actions() self.setStyleSheet(MAINWINDOW_SS) self.ui.tableView_resources.setModel(self.resources_model) self.ui.tableView_resources.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_resource_data.setModel(self.resource_data_model) self.ui.tableView_resource_data.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_resource_data.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.ui.tableView_fields.setModel(self.fields_model) self.ui.tableView_fields.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_fields.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.ui.tableView_foreign_keys.setModel(self.foreign_keys_model) self.ui.tableView_foreign_keys.verticalHeader().setDefaultSectionSize(self.default_row_height) self.ui.tableView_foreign_keys.horizontalHeader().setResizeContentsPrecision(self.visible_rows) self.connect_signals() self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle("{0}[*] - Spine datapackage manager".format(self.datapackage.base_path)) self.load_datapackage() @property def undo_stack(self): return self.undo_group.activeStack() @property def datapackage_path(self): return os.path.join(self.datapackage.base_path, "datapackage.json") def load_datapackage(self): self._file_watcher.addPaths(self.datapackage.sources) self.append_save_resource_actions() self.resources_model.refresh_model() first_index = self.resources_model.index(0, 0) if not first_index.isValid(): return self.ui.tableView_resources.selectionModel().setCurrentIndex(first_index, QItemSelectionModel.Select) def add_menu_actions(self): """Add extra menu actions.""" self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_resources.toggleViewAction()) self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_data.toggleViewAction()) self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_fields.toggleViewAction()) self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_foreign_keys.toggleViewAction()) undo_action = self.undo_group.createUndoAction(self) redo_action = self.undo_group.createRedoAction(self) undo_action.setShortcuts(QKeySequence.Undo) redo_action.setShortcuts(QKeySequence.Redo) undo_action.setIcon(QIcon(":/icons/menu_icons/undo.svg")) redo_action.setIcon(QIcon(":/icons/menu_icons/redo.svg")) before = self.ui.menuEdit.actions()[0] self.ui.menuEdit.insertAction(before, undo_action) self.ui.menuEdit.insertAction(before, redo_action) self.ui.menuEdit.insertSeparator(before) def connect_signals(self): """Connect signals to slots.""" self.msg.connect(self.add_message) self.msg_error.connect(self.add_error_message) self._file_watcher.directoryChanged.connect(self._handle_source_dir_changed) self._file_watcher.fileChanged.connect(self._handle_source_file_changed) self.ui.actionCopy.triggered.connect(self.copy) self.ui.actionPaste.triggered.connect(self.paste) self.ui.actionClose.triggered.connect(self.close) self.ui.actionSave_All.triggered.connect(self.save_all) self.ui.actionSave_datapackage.triggered.connect(self.save_datapackage) self.ui.menuEdit.aboutToShow.connect(self.refresh_copy_paste_actions) self.fields_model.dataChanged.connect(self._handle_fields_data_changed) self.undo_group.cleanChanged.connect(self.update_window_modified) checkbox_delegate = CheckBoxDelegate(self) checkbox_delegate.data_committed.connect(self.fields_model.setData) self.ui.tableView_fields.setItemDelegateForColumn(2, checkbox_delegate) foreign_keys_delegate = ForeignKeysDelegate(self) foreign_keys_delegate.data_committed.connect(self.foreign_keys_model.setData) self.ui.tableView_foreign_keys.setItemDelegate(foreign_keys_delegate) self.ui.tableView_resources.selectionModel().currentChanged.connect(self._handle_current_resource_changed) self.ui.tableView_foreign_keys.customContextMenuRequested.connect(self.show_foreign_keys_context_menu) self._foreign_keys_context_menu.addAction("Remove foreign key", self._remove_foreign_key) @Slot(bool) def update_window_modified(self, _clean=None): """Updates window modified status and save actions depending on the state of the undo stack.""" try: dirty_resource_indexes = { idx for idx in range(len(self.datapackage.resources)) if self.is_resource_dirty(idx) } dirty = bool(dirty_resource_indexes) self.setWindowModified(dirty) except RuntimeError: return self.ui.actionSave_datapackage.setEnabled(dirty) self.ui.actionSave_All.setEnabled(dirty) for idx, action in enumerate(self._save_resource_actions): dirty = idx in dirty_resource_indexes action.setEnabled(dirty) self.resources_model.update_resource_dirty(idx, dirty) def is_resource_dirty(self, resource_index): if resource_index in self._changed_source_indexes: return True try: return not self.undo_stacks[resource_index].isClean() except KeyError: return False def get_undo_stack(self, resource_index): if resource_index not in self.undo_stacks: self.undo_stacks[resource_index] = stack = QUndoStack(self.undo_group) stack.cleanChanged.connect(self.update_window_modified) return self.undo_stacks[resource_index] @Slot(str) def _handle_source_dir_changed(self, _path): if not self.datapackage.resources: self.load_datapackage() return self.datapackage.difference_infer(os.path.join(self.datapackage.base_path, '*.csv')) self._file_watcher.addPaths(self.datapackage.sources) self.append_save_resource_actions() self.resources_model.refresh_model() self.refresh_models() @Slot(str) def _handle_source_file_changed(self, path): for idx, source in enumerate(self.datapackage.sources): if os.path.normpath(source) == os.path.normpath(path): self._changed_source_indexes.add(idx) self.update_window_modified() break def append_save_resource_actions(self): new_actions = [] for resource_index in range(len(self._save_resource_actions), len(self.datapackage.resources)): resource = self.datapackage.resources[resource_index] action = QAction(f"Save '{os.path.basename(resource.source)}'") action.setEnabled(False) action.triggered.connect( lambda checked=False, resource_index=resource_index: self.save_resource(resource_index) ) new_actions.append(action) self.ui.menuFile.insertActions(self._before_save_all, new_actions) self._save_resource_actions += new_actions @Slot() def refresh_copy_paste_actions(self): """Adjusts copy and paste actions depending on which widget has the focus. """ self.ui.actionCopy.setEnabled(focused_widget_has_callable(self, "copy")) self.ui.actionPaste.setEnabled(focused_widget_has_callable(self, "paste")) @Slot(str) def add_message(self, msg): """Prepend regular message to status bar. Args: msg (str): String to show in QStatusBar """ self.notification_stack.push(msg) @Slot(str) def add_error_message(self, msg): """Show error message. Args: msg (str): String to show """ self.err_msg.showMessage(msg) @Slot(bool) def save_all(self, _=False): resource_paths = {k: r.source for k, r in enumerate(self.datapackage.resources) if self.is_resource_dirty(k)} all_paths = list(resource_paths.values()) + [self.datapackage_path] if not self.get_permission(*all_paths): return for k, path in resource_paths.items(): self._save_resource(k, path) self.save_datapackage() @Slot(bool) def save_datapackage(self, _=False): if self.datapackage.save(self.datapackage_path): self.msg.emit("'datapackage.json' succesfully saved") return self.msg_error.emit("Failed to save 'datapackage.json'") def save_resource(self, resource_index): resource = self.datapackage.resources[resource_index] filepath = resource.source if not self.get_permission(filepath, self.datapackage_path): return self._save_resource(resource_index, filepath) self.save_datapackage() def _save_resource(self, resource_index, filepath): headers = self.datapackage.resources[resource_index].schema.field_names self._file_watcher.removePath(filepath) with open(filepath, 'w', newline='') as csvfile: writer = csv.writer(csvfile) writer.writerow(headers) for row in self.datapackage.resource_data(resource_index): writer.writerow(row) self.msg.emit(f"'{os.path.basename(filepath)}' succesfully saved") self._file_watcher.addPath(filepath) self._changed_source_indexes.discard(resource_index) stack = self.undo_stacks.get(resource_index) if not stack or stack.isClean(): self.update_window_modified() elif stack: stack.setClean() def get_permission(self, *filepaths): start_dir = self.datapackage.base_path filepaths = [os.path.relpath(path, start_dir) for path in filepaths if os.path.isfile(path)] if not filepaths: return True pathlist = "".join([f"<li>{path}</li>" for path in filepaths]) msg = f"The following file(s) in <b>{os.path.basename(start_dir)}</b> will be replaced: <ul>{pathlist}</ul>. Are you sure?" message_box = QMessageBox( QMessageBox.Question, "Replacing file(s)", msg, QMessageBox.Ok | QMessageBox.Cancel, parent=self ) message_box.button(QMessageBox.Ok).setText("Replace") return message_box.exec_() != QMessageBox.Cancel @Slot(bool) def copy(self, checked=False): """Copies data to clipboard.""" call_on_focused_widget(self, "copy") @Slot(bool) def paste(self, checked=False): """Pastes data from clipboard.""" call_on_focused_widget(self, "paste") @Slot("QModelIndex", "QModelIndex") def _handle_current_resource_changed(self, current, _previous): """Resets resource data and schema models whenever a new resource is selected.""" self.refresh_models(current) def refresh_models(self, current=None): if current is None: current = self.ui.tableView_resources.selectionModel().currentIndex() if current.column() != 0 or current.row() == self.selected_resource_index: return self.selected_resource_index = current.row() self.get_undo_stack(self.selected_resource_index).setActive() self.resource_data_model.refresh_model(self.selected_resource_index) self.fields_model.refresh_model(self.selected_resource_index) self.foreign_keys_model.refresh_model(self.selected_resource_index) self.ui.tableView_resource_data.resizeColumnsToContents() self.ui.tableView_fields.resizeColumnsToContents() self.ui.tableView_foreign_keys.resizeColumnsToContents() @Slot("QModelIndex", "QModelIndex", "QVector<int>") def _handle_fields_data_changed(self, top_left, bottom_right, roles): top, left = top_left.row(), top_left.column() bottom, right = bottom_right.row(), bottom_right.column() if left <= 0 <= right and Qt.DisplayRole in roles: # Fields name changed self.resource_data_model.headerDataChanged.emit(Qt.Horizontal, top, bottom) self.ui.tableView_resource_data.resizeColumnsToContents() self.foreign_keys_model.emit_data_changed() @Slot("QPoint") def show_foreign_keys_context_menu(self, pos): index = self.ui.tableView_foreign_keys.indexAt(pos) if not index.isValid() or index.row() == index.model().rowCount() - 1: return global_pos = self.ui.tableView_foreign_keys.viewport().mapToGlobal(pos) self._foreign_keys_context_menu.popup(global_pos) @Slot(bool) def _remove_foreign_key(self, checked=False): index = self.ui.tableView_foreign_keys.currentIndex() if not index.isValid(): return index.model().call_remove_foreign_key(index.row()) def restore_ui(self): """Restore UI state from previous session.""" window_size = self.qsettings.value("dataPackageWidget/windowSize") window_pos = self.qsettings.value("dataPackageWidget/windowPosition") window_maximized = self.qsettings.value("dataPackageWidget/windowMaximized", defaultValue='false') window_state = self.qsettings.value("dataPackageWidget/windowState") n_screens = self.qsettings.value("mainWindow/n_screens", defaultValue=1) original_size = self.size() if window_size: self.resize(window_size) if window_pos: self.move(window_pos) # noinspection PyArgumentList if len(QGuiApplication.screens()) < int(n_screens): # There are less screens available now than on previous application startup self.move(0, 0) # Move this widget to primary screen position (0,0) ensure_window_is_on_screen(self, original_size) if window_maximized == 'true': self.setWindowState(Qt.WindowMaximized) if window_state: self.restoreState(window_state, version=1) # Toolbar and dockWidget positions def closeEvent(self, event=None): """Handle close event. Args: event (QEvent): Closing event if 'X' is clicked. """ # save qsettings self.qsettings.setValue("dataPackageWidget/windowSize", self.size()) self.qsettings.setValue("dataPackageWidget/windowPosition", self.pos()) self.qsettings.setValue("dataPackageWidget/windowState", self.saveState(version=1)) self.qsettings.setValue("dataPackageWidget/windowMaximized", self.windowState() == Qt.WindowMaximized) self.qsettings.setValue("dataPackageWidget/n_screens", len(QGuiApplication.screens())) if event: event.accept()
class QFileSystemLibraryModel(QFileSystemModel): """ File System Model for displaying QLibrary in MetalGUI Has additional FileWatcher added to keep track of edited QComponent files and, in developer mode, to alert the view/delegate to let the user know these files are dirty and refresh the design """ FILENAME = 0 # Column index to display filenames REBUILD = 1 # Column index to display Rebuild button file_dirtied_signal = Signal() file_cleaned_signal = Signal() def __init__(self, parent: QWidget = None): """ Initializes Model Args: parent(QWidget): Parent widget """ super().__init__(parent) self.file_system_watcher = QFileSystemWatcher() self.dirtied_files = {} self.ignored_substrings = {'.cpython', '__pycache__'} self.is_dev_mode = False self.columns = ['QComponents', 'Rebuild Buttons'] def is_valid_file(self, file: str) -> bool: """ Whether it's a file the FileWatcher should track Args: file(str): Filename Returns: bool: Whether file is one the FileWatcher should track """ for sub in self.ignored_substrings: if sub in file: return False return True def clean_file(self, filepath: str): """ Remove file from the dirtied_files dictionary and remove any parent files who are only dirty due to this file. Emits file_cleaned_signal. Args: filepath(str): File path of file to be cleaned """ filename = self.filepath_to_filename(filepath) self.dirtied_files.pop(filename, f"failed to pop {filepath}") sep = os.sep if os.sep in filepath else '/' for file in filepath.split(sep): if file in self.dirtied_files: # if file was in dirtied files only because it is a parent dir # of filename, remove self.dirtied_files[file].discard(filename) if len(self.dirtied_files[file]) < 1: self.dirtied_files.pop(file) self.file_cleaned_signal.emit() def dirty_file(self, filepath: str): """ Adds file and parent directories to the dirtied_files dictionary. Emits file_dirtied_signal Args: filepath (str): Dirty file path """ filename = self.filepath_to_filename(filepath) if not self.is_valid_file(filename): return sep = os.sep if os.sep in filepath else '/' for file in filepath.split(sep): if file in self.dirtied_files: self.dirtied_files[file].add(filename) else: self.dirtied_files[file] = {filename} # overwrite filename entry from above self.dirtied_files[filename] = {filepath} self.file_dirtied_signal.emit() def is_file_dirty(self, filepath: str) -> bool: """ Checks whether file is dirty Args: filepath (str): File in question Returns: bool: Whether file is dirty """ filename = self.filepath_to_filename(filepath) return filename in self.dirtied_files def filepath_to_filename(self, filepath: str) -> str: # pylint: disable=R0201, no-self-use """ Gets just the filename from the full filepath Args: filepath (str): Full file path Returns: str: Filename """ # split on os.sep and / because PySide appears to sometimes use / on # certain Windows filename = filepath.split(os.sep)[-1].split('/')[-1] if '.py' in filename: return filename[:-len('.py')] return filename def setRootPath(self, path: str) -> QModelIndex: """ Sets FileWatcher on root path and adds rootpath to model Args: path (str): Root path Returns: QModelIndex: Root index """ for root, _, files in os.walk(path): # do NOT use directory changed -- fails for some reason for name in files: self.file_system_watcher.addPath(os.path.join(root, name)) self.file_system_watcher.fileChanged.connect(self.alert_highlight_row) return super().setRootPath(path) def alert_highlight_row(self, filepath: str): """ Dirties file and re-adds edited file to the FileWatcher Args: filepath (str): Dirty file """ # ensure get only filename if filepath not in self.file_system_watcher.files(): if os.path.exists(filepath): self.file_system_watcher.addPath(filepath) self.dirty_file(filepath) def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> typing.Any: """ Set the headers to be displayed. Args: section (int): Section number orientation (Qt orientation): Section orientation role (Qt display role): Display role. Defaults to DisplayRole. Returns: typing.Any: The header data, or None if not found """ if role == Qt.DisplayRole: if not self.is_dev_mode and section == self.REBUILD: return "" if orientation == Qt.Horizontal: if section < len(self.columns): return self.columns[section] elif role == Qt.FontRole: if section == 0: font = QFont() font.setBold(True) return font return super().headerData(section, orientation, role) def set_file_is_dev_mode(self, ison: bool): """ Set dev_mode Args: ison(bool): Whether dev_mode is on """ self.is_dev_mode = ison
class QmlInstantEngine(QQmlApplicationEngine): """ QmlInstantEngine is an utility class helping developing QML applications. It reloads itself whenever one of the watched source files is modified. As it consumes resources, make sure to disable file watching in production mode. """ def __init__(self, sourceFile="", watching=True, verbose=False, parent=None): """ watching -- Defines whether the watcher is active (default: True) verbose -- if True, output log infos (default: False) """ super(QmlInstantEngine, self).__init__(parent) self._fileWatcher = QFileSystemWatcher() # Internal Qt File Watcher self._sourceFile = "" self._watchedFiles = [] # Internal watched files list self._verbose = verbose # Verbose bool self._watching = False # self._extensions = [ "qml", "js" ] # File extensions that defines files to watch when adding a folder self._rootItem = None def onObjectCreated(root, url): if not root: return # Restore root item geometry if self._rootItem: root.setGeometry(self._rootItem.geometry()) self._rootItem.deleteLater() self._rootItem = root self.objectCreated.connect(onObjectCreated) # Update the watching status self.setWatching(watching) if sourceFile: self.load(sourceFile) def load(self, sourceFile): self._sourceFile = sourceFile super(QmlInstantEngine, self).load(sourceFile) def setWatching(self, watchValue): """ Enable (True) or disable (False) the file watching. Tip: file watching should be enable only when developing. """ if self._watching is watchValue: return self._watching = watchValue # Enable the watcher if self._watching: # 1. Add internal list of files to the internal Qt File Watcher self.addFiles(self._watchedFiles) # 2. Connect 'filechanged' signal self._fileWatcher.fileChanged.connect(self.onFileChanged) # Disabling the watcher else: # 1. Remove all files in the internal Qt File Watcher self._fileWatcher.removePaths(self._watchedFiles) # 2. Disconnect 'filechanged' signal self._fileWatcher.fileChanged.disconnect(self.onFileChanged) @property def watchedExtensions(self): """ Returns the list of extensions used when using addFilesFromDirectory. """ return self._extensions @watchedExtensions.setter def watchedExtensions(self, extensions): """ Set the list of extensions to search for when using addFilesFromDirectory. """ self._extensions = extensions def setVerbose(self, verboseValue): """ Activate (True) or desactivate (False) the verbose. """ self._verbose = verboseValue def addFile(self, filename): """ Add the given 'filename' to the watched files list. 'filename' can be an absolute or relative path (str and QUrl accepted) """ # Deal with QUrl type # NOTE: happens when using the source() method on a QQuickView if isinstance(filename, QUrl): filename = filename.path() # Make sure the file exists if not os.path.isfile(filename): raise ValueError("addFile: file %s doesn't exist." % filename) # Return if the file is already in our internal list if filename in self._watchedFiles: return # Add this file to the internal files list self._watchedFiles.append(filename) # And, if watching is active, add it to the internal watcher as well if self._watching: if self._verbose: print("instantcoding: addPath", filename) self._fileWatcher.addPath(filename) def addFiles(self, filenames): """ Add the given 'filenames' to the watched files list. filenames -- a list of absolute or relative paths (str and QUrl accepted) """ # Convert to list if not isinstance(filenames, list): filenames = [filenames] for filename in filenames: self.addFile(filename) def addFilesFromDirectory(self, dirname, recursive=False): """ Add files from the given directory name 'dirname'. dirname -- an absolute or a relative path recursive -- if True, will search inside each subdirectories recursively. """ if not os.path.isdir(dirname): raise RuntimeError( "addFilesFromDirectory : %s is not a valid directory." % dirname) if recursive: for dirpath, dirnames, filenames in os.walk(dirname): for filename in filenames: # Removing the starting dot from extension if os.path.splitext(filename)[1][1:] in self._extensions: self.addFile(os.path.join(dirpath, filename)) else: filenames = os.listdir(dirname) filenames = [ os.path.join(dirname, filename) for filename in filenames if os.path.splitext(filename)[1][1:] in self._extensions ] self.addFiles(filenames) def removeFile(self, filename): """ Remove the given 'filename' from the watched file list. Tip: make sure to use relative or absolute path according to how you add this file. """ if filename in self._watchedFiles: self._watchedFiles.remove(filename) if self._watching: self._fileWatcher.removePath(filename) def getRegisteredFiles(self): """ Returns the list of watched files """ return self._watchedFiles @Slot(str) def onFileChanged(self, filepath): """ Handle changes in a watched file. """ if filepath not in self._watchedFiles: # could happen if a file has just been reloaded # and has not been re-added yet to the watched files return if self._verbose: print("Source file changed : ", filepath) # Clear the QQuickEngine cache self.clearComponentCache() # Remove the modified file from the watched list self.removeFile(filepath) cptTry = 0 # Make sure file is available before doing anything # NOTE: useful to handle editors (Qt Creator) that deletes the source file and # creates a new one when saving while not os.path.exists(filepath) and cptTry < 10: time.sleep(0.1) cptTry += 1 self.reload() # Finally, re-add the modified file to the watch system # after a short cooldown to avoid multiple consecutive reloads QTimer.singleShot(200, lambda: self.addFile(filepath)) def reload(self): print("Reloading {}".format(self._sourceFile)) self.load(self._sourceFile)
class MainWindow(QMainWindow): def __init__(self, path): super().__init__() self.widget = None if path is None: path = load_setting('file_path') if path is None: path = self.query_new_path() if path is None: print('No file selected, exiting') sys.exit(1) self.open_file(path) self.statusBar() open_action = QAction('&Open', self) open_action.setShortcut('Ctrl+O') open_action.setStatusTip('Open config file') open_action.triggered.connect(self.query_open) reload_action = QAction('&Reload', self) reload_action.setShortcut('Ctrl+R') reload_action.setStatusTip('Reload config file') reload_action.triggered.connect(self.query_reload) menubar = self.menuBar() fileMenu = menubar.addMenu('&File') fileMenu.addAction(open_action) fileMenu.addAction(reload_action) # copy_curl_action = QAction('Copy curl command', self) # copy_curl_action.setStatusTip('Copy curl command') # copy_curl_action.triggered.connect(self.copy_curl_command) # routeMenu = menubar.addMenu('&Route') # routeMenu.addAction(copy_curl_action) self.setFont(FONT) self.setWindowTitle('Caribou') self.setWindowIcon(QIcon(os.path.join(CURRENT_DIR, 'icon.png'))) def query_new_path(self): return QFileDialog.getOpenFileName(self, "Open File", os.path.expanduser("~"), "Python file (*.py)")[0] def query_open(self): path = self.query_new_path() if path is not None: self.open_file(path) def open_file(self, path): self.file_watcher = QFileSystemWatcher() self.file_watcher.addPath(path) self.file_watcher.fileChanged.connect(self.reload) save_setting('file_path', path) persist_storage() self.path = path self.reload(path) # def copy_curl_command(self): # pass def query_reload(self): return self.reload(self.path) def reload(self, path): current_route = self.widget.selected_route if self.widget is not None else None current_search = self.widget.current_search( ) if self.widget is not None else None assert path == self.path try: routes = load_file(self.path) except Exception as e: msgBox = QMessageBox() if isinstance(e, CaribouException): msgBox.setText(str(e)) else: msgBox.setText(traceback.format_exc()) msgBox.exec_() routes = [] if self.widget: self.widget.setParent(None) self.widget = MainWidget(routes) self.setCentralWidget(self.widget) if current_route is not None: self.widget.set_route_with_name(current_route.name) if current_search is not None: self.widget.set_search(current_search)
class DataConnection(ProjectItem): """Data Connection class. Attributes: toolbox (ToolboxUI): QMainWindow instance name (str): Object name description (str): Object description references (list): List of file references x (int): Initial X coordinate of item icon y (int): Initial Y coordinate of item icon """ def __init__(self, toolbox, name, description, references, x, y): """Class constructor.""" super().__init__(name, description) self._toolbox = toolbox self._project = self._toolbox.project() self.item_type = "Data Connection" # self._widget = DataConnectionWidget(self, self.item_type) self.reference_model = QStandardItemModel() # References to files self.data_model = QStandardItemModel( ) # Paths of project internal files. These are found in DC data directory self.datapackage_icon = QIcon(QPixmap(":/icons/datapkg.png")) self.data_dir_watcher = QFileSystemWatcher(self) # Make project directory for this Data Connection self.data_dir = os.path.join(self._project.project_dir, self.short_name) try: create_dir(self.data_dir) self.data_dir_watcher.addPath(self.data_dir) except OSError: self._toolbox.msg_error.emit( "[OSError] Creating directory {0} failed." " Check permissions.".format(self.data_dir)) # Populate references model self.references = references self.populate_reference_list(self.references) # Populate data (files) model data_files = self.data_files() self.populate_data_list(data_files) self._graphics_item = DataConnectionImage(self._toolbox, x - 35, y - 35, 70, 70, self.name) self.spine_datapackage_form = None # self.ui.toolButton_datapackage.setMenu(self.datapackage_popup_menu) # TODO: OBSOLETE? self._sigs = self.make_signal_handler_dict() def make_signal_handler_dict(self): """Returns a dictionary of all shared signals and their handlers. This is to enable simpler connecting and disconnecting.""" s = dict() s[self._toolbox.ui.toolButton_dc_open_dir. clicked] = self.open_directory s[self._toolbox.ui.toolButton_plus.clicked] = self.add_references s[self._toolbox.ui.toolButton_minus.clicked] = self.remove_references s[self._toolbox.ui.toolButton_add.clicked] = self.copy_to_project s[self._toolbox.ui.pushButton_datapackage. clicked] = self.show_spine_datapackage_form s[self._toolbox.ui.treeView_dc_references. doubleClicked] = self.open_reference s[self._toolbox.ui.treeView_dc_data. doubleClicked] = self.open_data_file s[self.data_dir_watcher.directoryChanged] = self.refresh s[self._toolbox.ui.treeView_dc_references. files_dropped] = self.add_files_to_references s[self._toolbox.ui.treeView_dc_data. files_dropped] = self.add_files_to_data_dir s[self._graphics_item.master().scene(). files_dropped_on_dc] = self.receive_files_dropped_on_dc return s def activate(self): """Restore selections and connect signals.""" self.restore_selections( ) # Do this before connecting signals or funny things happen super().connect_signals() def deactivate(self): """Save selections and disconnect signals.""" self.save_selections() if not super().disconnect_signals(): logging.error("Item {0} deactivation failed".format(self.name)) return False return True def restore_selections(self): """Restore selections into shared widgets when this project item is selected.""" self._toolbox.ui.label_dc_name.setText(self.name) self._toolbox.ui.treeView_dc_references.setModel(self.reference_model) self._toolbox.ui.treeView_dc_data.setModel(self.data_model) self.refresh() def save_selections(self): """Save selections in shared widgets for this project item into instance variables.""" pass def set_icon(self, icon): self._graphics_item = icon def get_icon(self): """Returns the item representing this data connection in the scene.""" return self._graphics_item @Slot("QVariant", name="add_files_to_references") def add_files_to_references(self, paths): """Add multiple file paths to reference list. Args: paths (list): A list of paths to files """ for path in paths: if path in self.references: self._toolbox.msg_warning.emit( "Reference to file <b>{0}</b> already available".format( path)) return self.references.append(os.path.abspath(path)) self.populate_reference_list(self.references) @Slot("QGraphicsItem", "QVariant", name="receive_files_dropped_on_dc") def receive_files_dropped_on_dc(self, item, file_paths): """Called when files are dropped onto a data connection graphics item. If the item is this Data Connection's graphics item, add the files to data.""" if item == self._graphics_item: self.add_files_to_data_dir(file_paths) @Slot("QVariant", name="add_files_to_data_dir") def add_files_to_data_dir(self, file_paths): """Add files to data directory""" for file_path in file_paths: src_dir, filename = os.path.split(file_path) self._toolbox.msg.emit( "Copying file <b>{0}</b> to <b>{1}</b>".format( filename, self.name)) try: shutil.copy(file_path, self.data_dir) except OSError: self._toolbox.msg_error.emit("[OSError] Copying failed") return data_files = self.data_files() self.populate_data_list(data_files) @Slot(bool, name="open_directory") def open_directory(self, checked=False): """Open file explorer in Data Connection data directory.""" url = "file:///" + self.data_dir # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = QDesktopServices.openUrl(QUrl(url, QUrl.TolerantMode)) if not res: self._toolbox.msg_error.emit( "Failed to open directory: {0}".format(self.data_dir)) @Slot(bool, name="add_references") def add_references(self, checked=False): """Let user select references to files for this data connection.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getOpenFileNames(self._toolbox, "Add file references", APPLICATION_PATH, "*.*") file_paths = answer[0] if not file_paths: # Cancel button clicked return for path in file_paths: if path in self.references: self._toolbox.msg_warning.emit( "Reference to file <b>{0}</b> already available".format( path)) continue self.references.append(os.path.abspath(path)) self.populate_reference_list(self.references) @Slot(bool, name="remove_references") def remove_references(self, checked=False): """Remove selected references from reference list. Do not remove anything if there are no references selected. """ indexes = self._toolbox.ui.treeView_dc_references.selectedIndexes() if not indexes: # Nothing selected self._toolbox.msg.emit("Please select references to remove") return else: rows = [ind.row() for ind in indexes] rows.sort(reverse=True) for row in rows: self.references.pop(row) self._toolbox.msg.emit("Selected references removed") self.populate_reference_list(self.references) @Slot(bool, name="copy_to_project") def copy_to_project(self, checked=False): """Copy selected file references to this Data Connection's data directory.""" selected_indexes = self._toolbox.ui.treeView_dc_references.selectedIndexes( ) if len(selected_indexes) == 0: self._toolbox.msg_warning.emit("No files to copy") return for index in selected_indexes: file_path = self.reference_model.itemFromIndex(index).data( Qt.DisplayRole) if not os.path.exists(file_path): self._toolbox.msg_error.emit( "File <b>{0}</b> does not exist".format(file_path)) continue src_dir, filename = os.path.split(file_path) self._toolbox.msg.emit( "Copying file <b>{0}</b> to Data Connection <b>{1}</b>".format( filename, self.name)) try: shutil.copy(file_path, self.data_dir) except OSError: self._toolbox.msg_error.emit("[OSError] Copying failed") continue @Slot("QModelIndex", name="open_reference") def open_reference(self, index): """Open reference in default program.""" if not index: return if not index.isValid(): logging.error("Index not valid") return else: reference = self.file_references()[index.row()] url = "file:///" + reference # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = QDesktopServices.openUrl(QUrl(url, QUrl.TolerantMode)) if not res: self._toolbox.msg_error.emit( "Failed to open reference:<b>{0}</b>".format(reference)) @Slot("QModelIndex", name="open_data_file") def open_data_file(self, index): """Open data file in default program.""" if not index: return if not index.isValid(): logging.error("Index not valid") return else: data_file = self.data_files()[index.row()] url = "file:///" + os.path.join(self.data_dir, data_file) # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = QDesktopServices.openUrl(QUrl(url, QUrl.TolerantMode)) if not res: self._toolbox.msg_error.emit( "Opening file <b>{0}</b> failed".format(data_file)) @busy_effect def show_spine_datapackage_form(self): """Show spine_datapackage_form widget.""" if self.spine_datapackage_form: if self.spine_datapackage_form.windowState() & Qt.WindowMinimized: # Remove minimized status and restore window with the previous state (maximized/normal state) self.spine_datapackage_form.setWindowState( self.spine_datapackage_form.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.spine_datapackage_form.activateWindow() else: self.spine_datapackage_form.raise_() return self.spine_datapackage_form = SpineDatapackageWidget(self) self.spine_datapackage_form.destroyed.connect( self.datapackage_form_destroyed) self.spine_datapackage_form.show() @Slot(name="datapackage_form_destroyed") def datapackage_form_destroyed(self): self.spine_datapackage_form = None def make_new_file(self): """Create a new blank file to this Data Connections data directory.""" msg = "File name" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText(self._toolbox, "Create new file", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint) file_name = answer[0] if not file_name: # Cancel button clicked return if file_name.strip() == "": return # Check that file name has no invalid chars if any(True for x in file_name if x in INVALID_FILENAME_CHARS): msg = "File name <b>{0}</b> contains invalid characters.".format( file_name) # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self._toolbox, "Creating file failed", msg) return file_path = os.path.join(self.data_dir, file_name) if os.path.exists(file_path): msg = "File <b>{0}</b> already exists.".format(file_name) # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self._toolbox, "Creating file failed", msg) return try: with open(file_path, "w") as fp: self._toolbox.msg.emit( "File <b>{0}</b> created to Data Connection <b>{1}</b>". format(file_name, self.name)) except OSError: msg = "Please check directory permissions." # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self._toolbox, "Creating file failed", msg) return def remove_files(self): """Remove selected files from data directory.""" indexes = self._toolbox.ui.treeView_dc_data.selectedIndexes() if not indexes: # Nothing selected self._toolbox.msg.emit("Please select files to remove") return else: file_list = list() for index in indexes: file_at_index = self.data_model.itemFromIndex(index).data( Qt.DisplayRole) file_list.append(file_at_index) files = "\n".join(file_list) msg = "The following files will be removed permanently from the project\n\n" \ "{0}\n\n" \ "Are you sure?".format(files) # noinspection PyCallByClass, PyTypeChecker answer = QMessageBox.question( self._toolbox, "Remove {0} file(s)?".format(len(file_list)), msg, QMessageBox.Yes, QMessageBox.No) if not answer == QMessageBox.Yes: return for filename in file_list: path_to_remove = os.path.join(self.data_dir, filename) try: os.remove(path_to_remove) self._toolbox.msg.emit( "File <b>{0}</b> removed".format(path_to_remove)) except OSError: self._toolbox.msg_error.emit( "Removing file {0} failed.\nCheck permissions.".format( path_to_remove)) return def file_references(self): """Return a list of paths to files that are in this item as references.""" return self.references def data_files(self): """Return a list of files that are in the data directory.""" if not os.path.isdir(self.data_dir): return None return os.listdir(self.data_dir) @Slot(name="refresh") def refresh(self): """Refresh data files QTreeView. NOTE: Might lead to performance issues.""" d = self.data_files() self.populate_data_list(d) def find_file(self, fname, visited_items): """Search for filename in references and data and return the path if found. Args: fname (str): File name (no path) visited_items (list): List of project item names that have been visited Returns: Full path to file that matches the given file name or None if not found. """ # logging.debug("Looking for file {0} in DC {1}.".format(fname, self.name)) if self in visited_items: self._toolbox.msg_warning.emit( "There seems to be an infinite loop in your project. Please fix the " "connections and try again. Detected at {0}.".format( self.name)) return None if fname in self.data_files(): # logging.debug("{0} found in DC {1}".format(fname, self.name)) self._toolbox.msg.emit( "\t<b>{0}</b> found in Data Connection <b>{1}</b>".format( fname, self.name)) path = os.path.join(self.data_dir, fname) return path for path in self.file_references( ): # List of paths including file name p, fn = os.path.split(path) if fn == fname: # logging.debug("{0} found in DC {1}".format(fname, self.name)) self._toolbox.msg.emit( "\tReference for <b>{0}</b> found in Data Connection <b>{1}</b>" .format(fname, self.name)) return path visited_items.append(self) for input_item in self._toolbox.connection_model.input_items( self.name): # Find item from project model found_index = self._toolbox.project_item_model.find_item( input_item) if not found_index: self._toolbox.msg_error.emit( "Item {0} not found. Something is seriously wrong.".format( input_item)) continue item = self._toolbox.project_item_model.project_item(found_index) if item.item_type in ["Data Store", "Data Connection"]: path = item.find_file(fname, visited_items) if path is not None: return path return None def find_files(self, pattern, visited_items): """Search for files matching the given pattern (with wildcards) in references and data and return a list of matching paths. Args: pattern (str): File name (no path). May contain wildcards. visited_items (list): List of project item names that have been visited Returns: List of matching paths. List is empty if no matches found. """ paths = list() if self in visited_items: self._toolbox.msg_warning.emit( "There seems to be an infinite loop in your project. Please fix the " "connections and try again. Detected at {0}.".format( self.name)) return paths # Search files that match the pattern from this Data Connection's data directory for data_file in self.data_files( ): # data_file is a filename (no path) if fnmatch.fnmatch(data_file, pattern): # self._toolbox.msg.emit("\t<b>{0}</b> matches pattern <b>{1}</b> in Data Connection <b>{2}</b>" # .format(data_file, pattern, self.name)) path = os.path.join(self.data_dir, data_file) paths.append(path) # Search files that match the pattern from this Data Connection's references for ref_file in self.file_references( ): # List of paths including file name p, fn = os.path.split(ref_file) if fnmatch.fnmatch(fn, pattern): # self._toolbox.msg.emit("\tReference <b>{0}</b> matches pattern <b>{1}</b> " # "in Data Connection <b>{2}</b>".format(fn, pattern, self.name)) paths.append(ref_file) visited_items.append(self) # Find items that are connected to this Data Connection for input_item in self._toolbox.connection_model.input_items( self.name): found_index = self._toolbox.project_item_model.find_item( input_item) if not found_index: self._toolbox.msg_error.emit( "Item {0} not found. Something is seriously wrong.".format( input_item)) continue item = self._toolbox.project_item_model.project_item(found_index) if item.item_type in ["Data Store", "Data Connection"]: matching_paths = item.find_files(pattern, visited_items) if matching_paths is not None: paths = paths + matching_paths return paths return paths def populate_reference_list(self, items): """List file references in QTreeView. If items is None or empty list, model is cleared. """ self.reference_model.clear() self.reference_model.setHorizontalHeaderItem( 0, QStandardItem("References")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) qitem.setData(item, Qt.ToolTipRole) qitem.setData( self._toolbox.style().standardIcon(QStyle.SP_FileLinkIcon), Qt.DecorationRole) self.reference_model.appendRow(qitem) def populate_data_list(self, items): """List project internal data (files) in QTreeView. If items is None or empty list, model is cleared. """ self.data_model.clear() self.data_model.setHorizontalHeaderItem( 0, QStandardItem("Data")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) if item == 'datapackage.json': qitem.setData(self.datapackage_icon, Qt.DecorationRole) else: qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) full_path = os.path.join(self.data_dir, item) # For drag and drop qitem.setData(full_path, Qt.UserRole) self.data_model.appendRow(qitem) def update_name_label(self): """Update Data Connection tab name label. Used only when renaming project items.""" self._toolbox.ui.label_dc_name.setText(self.name)
class Firefox(QObject): NAME: str = "Firefox" SESSION_LOCATION_COMMAND: list = [ "find ~/.mozilla/firefox*/*.*/sessionstore-backups/recovery.jsonlz4" ] MOZILLA_MAGIC_NUMBER: int = 8 # NOTE: https://gist.github.com/mnordhoff/25e42a0d29e5c12785d0 tabs_changed = Signal() def __init__(self): super(Firefox, self).__init__(None) self.logger = create_logger(__name__) self.tabs_model = WebTabsModel() self.file_expect = FileExpect() self.detect() self.file_expect.file_exists.connect(self.get_tabs) def detect(self) -> None: try: self.tabs_location = subprocess.check_output( Firefox.SESSION_LOCATION_COMMAND, shell=True).decode("utf-8").replace("\n", "") self.logger.info("Firefox detected={tabs_location}".format( tabs_location=(bool(self.tabs_location != "")))) self.tabs_file_watcher = QFileSystemWatcher() self.get_tabs(self.tabs_location) self.tabs_file_watcher.fileChanged.connect(self.get_tabs, Qt.QueuedConnection) self.detected = True except subprocess.CalledProcessError as error: self.detected = False @Slot(str) def get_tabs(self, path: str) -> None: tabs = [] if not os.path.isfile(path): self.file_expect.observe(path) return if path not in (self.tabs_file_watcher.files()): self.tabs_file_watcher.addPath(self.tabs_location) with open(self.tabs_location, "rb") as tabs_file: mozilla_magic = tabs_file.read(Firefox.MOZILLA_MAGIC_NUMBER) j_data = json.loads( lz4.block.decompress(tabs_file.read()).decode("utf-8")) for window in j_data.get("windows"): for tab in window.get("tabs"): index = int(tab.get("index")) - 1 if (is_youtube(tab.get("entries")[index].get("url"))): tabs.append( BrowserTab( tab.get("entries")[index].get("url"), tab.get("entries")[index].get("title"))) if (self.tabs_model.tabs != tabs): self.tabs_model.set_tabs(tabs) @Property(str, constant=True) def name(self) -> str: return Firefox.NAME @Property(QObject, constant=True) def tabs(self) -> WebTabsModel: return self.tabs_model
class DataConnection(ProjectItem): def __init__(self, toolbox, project, logger, name, description, x, y, references=None): """Data Connection class. Args: toolbox (ToolboxUI): QMainWindow instance project (SpineToolboxProject): the project this item belongs to logger (LoggerInterface): a logger instance name (str): Object name description (str): Object description x (float): Initial X coordinate of item icon y (float): Initial Y coordinate of item icon references (list): a list of file paths """ super().__init__(name, description, x, y, project, logger) self._toolbox = toolbox self.reference_model = QStandardItemModel() # References to files self.data_model = QStandardItemModel( ) # Paths of project internal files. These are found in DC data directory self.datapackage_icon = QIcon(QPixmap(":/icons/datapkg.png")) self.data_dir_watcher = None # Populate references model if references is None: references = list() # Convert relative paths to absolute self.references = [ deserialize_path(r, self._project.project_dir) for r in references ] self.populate_reference_list(self.references) # Populate data (files) model data_files = self.data_files() self.populate_data_list(data_files) self.spine_datapackage_form = None def set_up(self): self.data_dir_watcher = QFileSystemWatcher(self) if os.path.isdir(self.data_dir): self.data_dir_watcher.addPath(self.data_dir) self.data_dir_watcher.directoryChanged.connect(self.refresh) @staticmethod def item_type(): """See base class.""" return ItemInfo.item_type() @staticmethod def item_category(): """See base class.""" return ItemInfo.item_category() def execution_item(self): """Creates DataConnection's execution counterpart.""" data_files = [ os.path.join(self.data_dir, f) for f in self.data_files() ] return ExecutableItem(self.name, self.file_references(), data_files, self._logger) def make_signal_handler_dict(self): """Returns a dictionary of all shared signals and their handlers. This is to enable simpler connecting and disconnecting.""" s = super().make_signal_handler_dict() # pylint: disable=unnecessary-lambda s[self._properties_ui.toolButton_dc_open_dir. clicked] = lambda checked=False: self.open_directory() s[self._properties_ui.toolButton_plus.clicked] = self.add_references s[self._properties_ui.toolButton_minus. clicked] = self.remove_references s[self._properties_ui.toolButton_add.clicked] = self.copy_to_project s[self._properties_ui.pushButton_datapackage. clicked] = self.show_spine_datapackage_form s[self._properties_ui.treeView_dc_references. doubleClicked] = self.open_reference s[self._properties_ui.treeView_dc_data. doubleClicked] = self.open_data_file s[self._properties_ui.treeView_dc_references. files_dropped] = self.add_files_to_references s[self._properties_ui.treeView_dc_data. files_dropped] = self.add_files_to_data_dir s[self.get_icon(). files_dropped_on_icon] = self.receive_files_dropped_on_icon s[self._properties_ui.treeView_dc_references. del_key_pressed] = lambda: self.remove_references() s[self._properties_ui.treeView_dc_data. del_key_pressed] = lambda: self.remove_files() return s def restore_selections(self): """Restore selections into shared widgets when this project item is selected.""" self._properties_ui.label_dc_name.setText(self.name) self._properties_ui.treeView_dc_references.setModel( self.reference_model) self._properties_ui.treeView_dc_data.setModel(self.data_model) @Slot("QVariant") def add_files_to_references(self, paths): """Add multiple file paths to reference list. Args: paths (list): A list of paths to files """ repeated_paths = [] new_paths = [] for path in paths: if any(os.path.samefile(path, ref) for ref in self.references): repeated_paths.append(path) else: new_paths.append(path) repeated_paths = ", ".join(repeated_paths) if repeated_paths: self._logger.msg_warning.emit( f"Reference to file(s) <b>{repeated_paths}</b> already available" ) if new_paths: self._toolbox.undo_stack.push( AddDCReferencesCommand(self, new_paths)) def do_add_files_to_references(self, paths): abspaths = [os.path.abspath(path) for path in paths] self.references.extend(abspaths) self.populate_reference_list(self.references) @Slot("QGraphicsItem", list) def receive_files_dropped_on_icon(self, icon, file_paths): """Called when files are dropped onto a data connection graphics item. If the item is this Data Connection's graphics item, add the files to data.""" if icon == self.get_icon(): self.add_files_to_data_dir(file_paths) @Slot("QVariant") def add_files_to_data_dir(self, file_paths): """Add files to data directory""" for file_path in file_paths: filename = os.path.split(file_path)[1] self._logger.msg.emit( f"Copying file <b>{filename}</b> to <b>{self.name}</b>") try: shutil.copy(file_path, self.data_dir) except OSError: self._logger.msg_error.emit("[OSError] Copying failed") return @Slot(bool) def add_references(self, checked=False): """Let user select references to files for this data connection.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getOpenFileNames(self._toolbox, "Add file references", self._project.project_dir, "*.*") file_paths = answer[0] if not file_paths: # Cancel button clicked return self.add_files_to_references(file_paths) @Slot(bool) def remove_references(self, checked=False): """Remove selected references from reference list. Do not remove anything if there are no references selected. """ indexes = self._properties_ui.treeView_dc_references.selectedIndexes() if not indexes: # Nothing selected self._logger.msg.emit("Please select references to remove") return references = [ind.data(Qt.DisplayRole) for ind in indexes] self._toolbox.undo_stack.push( RemoveDCReferencesCommand(self, references)) self._logger.msg.emit("Selected references removed") def do_remove_references(self, references): self.references = [ r for r in self.references if not any(os.path.samefile(r, ref) for ref in references) ] self.populate_reference_list(self.references) @Slot(bool) def copy_to_project(self, checked=False): """Copy selected file references to this Data Connection's data directory.""" selected_indexes = self._properties_ui.treeView_dc_references.selectedIndexes( ) if not selected_indexes: self._logger.msg_warning.emit("No files to copy") return for index in selected_indexes: file_path = self.reference_model.itemFromIndex(index).data( Qt.DisplayRole) if not os.path.exists(file_path): self._logger.msg_error.emit( f"File <b>{file_path}</b> does not exist") continue filename = os.path.split(file_path)[1] self._logger.msg.emit( f"Copying file <b>{filename}</b> to Data Connection <b>{self.name}</b>" ) try: shutil.copy(file_path, self.data_dir) except OSError: self._logger.msg_error.emit("[OSError] Copying failed") continue @Slot("QModelIndex") def open_reference(self, index): """Open reference in default program.""" if not index: return if not index.isValid(): logging.error("Index not valid") return reference = self.file_references()[index.row()] url = "file:///" + reference # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = open_url(url) if not res: self._logger.msg_error.emit( f"Failed to open reference:<b>{reference}</b>") @Slot("QModelIndex") def open_data_file(self, index): """Open data file in default program.""" if not index: return if not index.isValid(): logging.error("Index not valid") return data_file = self.data_files()[index.row()] url = "file:///" + os.path.join(self.data_dir, data_file) # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = open_url(url) if not res: self._logger.msg_error.emit( f"Opening file <b>{data_file}</b> failed") @busy_effect def show_spine_datapackage_form(self): """Show spine_datapackage_form widget.""" if self.spine_datapackage_form: if self.spine_datapackage_form.windowState() & Qt.WindowMinimized: # Remove minimized status and restore window with the previous state (maximized/normal state) self.spine_datapackage_form.setWindowState( self.spine_datapackage_form.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.spine_datapackage_form.activateWindow() else: self.spine_datapackage_form.raise_() return self.spine_datapackage_form = SpineDatapackageWidget(self) self.spine_datapackage_form.destroyed.connect( self.datapackage_form_destroyed) self.spine_datapackage_form.show() @Slot() def datapackage_form_destroyed(self): """Notify a connection that datapackage form has been destroyed.""" self.spine_datapackage_form = None def make_new_file(self): """Create a new blank file to this Data Connections data directory.""" msg = "File name" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText(self._toolbox, "Create new file", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint) file_name = answer[0] if not file_name.strip(): return # Check that file name has no invalid chars if any(True for x in file_name if x in INVALID_FILENAME_CHARS): msg = f"File name <b>{file_name}</b> contains invalid characters." self._logger.information_box.emit("Creating file failed", msg) return file_path = os.path.join(self.data_dir, file_name) if os.path.exists(file_path): msg = f"File <b>{file_name}</b> already exists." self._logger.information_box.emit("Creating file failed", msg) return try: with open(file_path, "w"): self._logger.msg.emit( f"File <b>{file_name}</b> created to Data Connection <b>{self.name}</b>" ) except OSError: msg = "Please check directory permissions." self._logger.information_box.emit("Creating file failed", msg) return def remove_files(self): """Remove selected files from data directory.""" indexes = self._properties_ui.treeView_dc_data.selectedIndexes() if not indexes: # Nothing selected self._logger.msg.emit("Please select files to remove") return file_list = list() for index in indexes: file_at_index = self.data_model.itemFromIndex(index).data( Qt.DisplayRole) file_list.append(file_at_index) files = "\n".join(file_list) msg = ( "The following files will be removed permanently from the project\n\n" "{0}\n\n" "Are you sure?".format(files)) title = "Remove {0} File(s)".format(len(file_list)) message_box = QMessageBox(QMessageBox.Question, title, msg, QMessageBox.Ok | QMessageBox.Cancel, parent=self._toolbox) message_box.button(QMessageBox.Ok).setText("Remove Files") answer = message_box.exec_() if answer == QMessageBox.Cancel: return for filename in file_list: path_to_remove = os.path.join(self.data_dir, filename) try: os.remove(path_to_remove) self._logger.msg.emit(f"File <b>{path_to_remove}</b> removed") except OSError: self._logger.msg_error.emit( f"Removing file {path_to_remove} failed.\nCheck permissions." ) return def file_references(self): """Returns a list of paths to files that are in this item as references.""" return self.references def data_files(self): """Returns a list of files that are in the data directory.""" if not os.path.isdir(self.data_dir): return [] files = list() with os.scandir(self.data_dir) as scan_iterator: for entry in scan_iterator: if entry.is_file(): files.append(entry.path) return files @Slot("QString") def refresh(self, _=None): """Refresh data files in Data Connection Properties. NOTE: Might lead to performance issues.""" d = self.data_files() self.populate_data_list(d) def populate_reference_list(self, items, emit_item_changed=True): """List file references in QTreeView. If items is None or empty list, model is cleared. """ self.reference_model.clear() self.reference_model.setHorizontalHeaderItem( 0, QStandardItem("References")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) qitem.setData(item, Qt.ToolTipRole) qitem.setData( self._toolbox.style().standardIcon(QStyle.SP_FileLinkIcon), Qt.DecorationRole) self.reference_model.appendRow(qitem) if emit_item_changed: self.item_changed.emit() def populate_data_list(self, items): """List project internal data (files) in QTreeView. If items is None or empty list, model is cleared. """ self.data_model.clear() self.data_model.setHorizontalHeaderItem( 0, QStandardItem("Data")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) if item == 'datapackage.json': qitem.setData(self.datapackage_icon, Qt.DecorationRole) else: qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) full_path = os.path.join(self.data_dir, item) # For drag and drop qitem.setData(full_path, Qt.UserRole) self.data_model.appendRow(qitem) self.item_changed.emit() def update_name_label(self): """Update Data Connection tab name label. Used only when renaming project items.""" self._properties_ui.label_dc_name.setText(self.name) def resources_for_direct_successors(self): """see base class""" refs = self.file_references() f_list = [os.path.join(self.data_dir, f) for f in self.data_files()] resources = [ ProjectItemResource(self, "file", url=pathlib.Path(ref).as_uri()) for ref in refs + f_list ] return resources def _do_handle_dag_changed(self, resources): """See base class.""" if not self.file_references() and not self.data_files(): self.add_notification( "This Data Connection does not have any references or data. " "Add some in the Data Connection Properties panel.") def item_dict(self): """Returns a dictionary corresponding to this item.""" d = super().item_dict() # Convert paths to relative before saving d["references"] = [ serialize_path(f, self._project.project_dir) for f in self.file_references() ] return d def rename(self, new_name): """Rename this item. Args: new_name (str): New name Returns: bool: True if renaming succeeded, False otherwise """ dirs = self.data_dir_watcher.directories() if dirs: self.data_dir_watcher.removePaths(dirs) if not super().rename(new_name): self.data_dir_watcher.addPaths(dirs) return False self.data_dir_watcher.addPath(self.data_dir) self.refresh() return True def tear_down(self): """Tears down this item. Called by toolbox just before closing. Closes the SpineDatapackageWidget instances opened.""" if self.spine_datapackage_form: self.spine_datapackage_form.close() watched_paths = self.data_dir_watcher.directories() if watched_paths: self.data_dir_watcher.removePaths(watched_paths) self.data_dir_watcher.deleteLater() def notify_destination(self, source_item): """See base class.""" if source_item.item_type() == "Tool": self._logger.msg.emit( f"Link established. Tool <b>{source_item.name}</b> output files will be " f"passed as references to item <b>{self.name}</b> after execution." ) elif source_item.item_type() in ["Data Store", "Importer"]: # Does this type of link do anything? self._logger.msg.emit("Link established.") else: super().notify_destination(source_item) @staticmethod def default_name_prefix(): """See base class.""" return "Data Connection"
class WebView(QWebEngineView): # Tell PageWidget that a file is dropped onto view. dropped_relay = Signal(QDropEvent) def __init__(self, image_path): ''' Execution order: parent's __init__ -> self.event() -> rest code in self.__init__ So, we must initialize at here, otherwise it will override value which is assigned in self.event(). ''' self.child_obj = None super(WebView, self).__init__(None) self.load(QUrl(image_path)) self.watcher = QFileSystemWatcher() self.watcher.fileChanged.connect(self.refresh_image) # Register file watcher self.watcher.addPath(image_path) self.setContextMenuPolicy(Qt.NoContextMenu) # Disable right click context menu def dragEnterEvent(self, drag_enter_event): # QDragEnterEvent if drag_enter_event.mimeData().hasUrls(): drag_enter_event.acceptProposedAction() # https://stackoverflow.com/a/4421835/4112667 def dragMoveEvent(self, event): pass def dropEvent(self, drop_event): # QDropEvent self.dropped_relay.emit(drop_event) def refresh_image(self, image_path): if not os.path.isfile(image_path): return self.reload() ''' https://forum.qt.io/post/549615 This bug causes the mouse event is captured by child widget of QWebEngineView We register a filter to capture what we need. https://stackoverflow.com/a/33576854/4112667 ''' def event(self, event): if event.type() == QEvent.ChildAdded: obj = event.child() if obj is not None and type(obj) == QWidget: self.child_obj = obj self.child_obj.installEventFilter(self) return QWebEngineView.event(self, event) def eventFilter(self, obj, event): if obj == self.child_obj and event.type() == QEvent.Wheel: self.do_wheel(event) return True elif obj == self.child_obj and event.type() == QEvent.MouseButtonPress: self.start_mouse_pos = event.localPos() self.start_scroll_pos = self.page().scrollPosition() return QWebEngineView.eventFilter(self, obj, event) # Make scrollbar work normally elif obj == self.child_obj and event.type() == QEvent.MouseMove: # Only process mouse move with left button pressed if event.buttons() != Qt.LeftButton: return QWebEngineView.eventFilter(self, obj, event) # If Ctrl is pressed, use the default event handler. This enable select text. if QGuiApplication.queryKeyboardModifiers() == Qt.ControlModifier: return QWebEngineView.eventFilter(self, obj, event) ''' OK. This is a mouse drag without Ctrl pressed. We will change it's behavior to something like ImageView. ''' current_mouse_pos = event.localPos() delta = current_mouse_pos - self.start_mouse_pos ''' Note: if mouse moves down 5 pixels, it's y-pos increase 5 pixels. The page should also move down 5 pixels which means the scrollbar should move up 5 pixels and the scroll position should decrease. ''' target_scroll_pos = self.start_scroll_pos - delta ''' Verify value range. I don't know if this is a must or whether this has any effects or not. ''' target_scroll_x = target_scroll_pos.x() target_scroll_y = target_scroll_pos.y() if target_scroll_x < 0: target_scroll_x = 0 if target_scroll_y < 0: target_scroll_y = 0 if target_scroll_x > self.page().contentsSize().width(): target_scroll_x = self.page().contentsSize().width() if target_scroll_y > self.page().contentsSize().height(): target_scroll_y = self.page().contentsSize().height() ''' We notice that if the page is zoomed in, the scrollbar moves faster than the mouse cursor. Besides, the scrollbar may jump a large distance towards the end in a weird way if you place the mouse cursor at the right bottom corner and try to drag the page towards the left upper corner several times. I don't know why but I print the target_scroll_pos which seems indeed correct. So I guess this may be caused by the scrollTo function in JavaScript. I think this function doesn't do things as I thought it would. Now that the page scrolls faster than mouse cursor dragging only after the page is zoomed in and it seems they have multiples relation, I guess if I divide the target_scroll_pos by zoomFactor, the page scrolling speed may be slow down and maybe scrollTo can scroll the page to correct position. Finally, this way is indeed the solution. However, I don't know why. If the page is zoomed out, it would be the opposite. The page scrolling speed should be sped up to match the mouse cursor. ''' target_scroll_x /= self.zoomFactor() target_scroll_y /= self.zoomFactor() # https://forum.qt.io/topic/60091/scroll-a-qwebengineview/3 self.page().runJavaScript(f"window.scrollTo({target_scroll_x}, {target_scroll_y})") return QWebEngineView.eventFilter(self, obj, event) # Make scrollbar work normally elif obj == self.child_obj and event.type() == QEvent.MouseButtonDblClick and event.buttons() == Qt.RightButton: self.setZoomFactor(1) return True return QWebEngineView.eventFilter(self, obj, event) def do_wheel(self, wheel_event): # QWheelEvent num_degrees = wheel_event.angleDelta().y() / 8 num_steps = num_degrees / 15 coefficient = 1 + (num_steps * 0.25) self.setZoomFactor(self.zoomFactor() * coefficient)
def __init__(self, app, parent=None): super(MainWindow, self).__init__(parent) self.imagesDir = app.dir + '/images/' self.setWindowIcon(QIcon(self.imagesDir + 'icon.png')) self.path = '' self.settings = QSettings() self.lastDir = self.settings.value('lastDir', '') self.setMinimumWidth(540) self.supportedFormats = [] for f in QImageReader.supportedImageFormats(): self.supportedFormats.append(str(f.data(), encoding="utf-8")) self.fileWatcher = QFileSystemWatcher() self.fileWatcher.fileChanged.connect(self.fileChanged) # widgets self.showPixmapWidget = None self.tileWidthSpinBox = QSpinBox() self.tileWidthSpinBox.setValue(16) self.tileWidthSpinBox.setFixedWidth(50) self.tileWidthSpinBox.setMinimum(1) self.tileHeightSpinBox = QSpinBox() self.tileHeightSpinBox.setValue(16) self.tileHeightSpinBox.setFixedWidth(50) self.tileHeightSpinBox.setMinimum(1) self.paddingSpinBox = QSpinBox() self.paddingSpinBox.setFixedWidth(50) self.paddingSpinBox.setMinimum(1) self.transparentCheckbox = QCheckBox("Transparent") self.transparentCheckbox.setChecked(True) self.transparentCheckbox.stateChanged.connect(self.transparentChanged) self.backgroundColorEdit = ColorEdit() self.backgroundColorEdit.setEnabled(False) self.backgroundColorLabel = QLabel("Background color:") self.backgroundColorLabel.setEnabled(False) self.forcePotCheckBox = QCheckBox("Force PoT") self.forcePotCheckBox.setChecked(True) self.forcePotCheckBox.stateChanged.connect(self.forcePotChanged) self.reorderTilesCheckBox = QCheckBox("Reorder tiles") self.generateAndExportButton = QPushButton("Generate and export") self.generateAndExportButton.setFixedHeight(32) self.generateAndExportButton.clicked.connect(self.generateAndExportClicked) self.generateAndExportButton.setEnabled(False) self.pixmapWidget = PixmapWidget() self.pixmapWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.pixmapWidget.setPixmap(self.createDropTextPixmap()) self.pixmapWidget.dropSignal.connect(self.fileDropped) self.pixmapWidget.setMinimumHeight(300) # load settings self.tileWidthSpinBox.setValue(int(self.settings.value('tileWidth', 16))) self.tileHeightSpinBox.setValue(int(self.settings.value('tileHeight', 16))) self.paddingSpinBox.setValue(int(self.settings.value('padding', 1))) self.forcePotCheckBox.setChecked(True if self.settings.value('forcePot', 'true') == 'true' else False) self.reorderTilesCheckBox.setChecked(True if self.settings.value('reorderTiles', 'false') == 'true' else False) self.transparentCheckbox.setChecked(True if self.settings.value('transparent', 'false') == 'true' else False) self.backgroundColorEdit.setColorText(str(self.settings.value('backgroundColor', '#FF00FF'))) self.restoreGeometry(QByteArray(self.settings.value('MainWindow/geometry'))) self.restoreState(QByteArray(self.settings.value('MainWindow/windowState'))) # layout hl1 = QHBoxLayout() hl1.setContentsMargins(5, 5, 5, 5) hl1.addWidget(QLabel("Tile width:")) hl1.addSpacing(5) hl1.addWidget(self.tileWidthSpinBox) hl1.addSpacing(15) hl1.addWidget(QLabel("Tile height:")) hl1.addSpacing(5) hl1.addWidget(self.tileHeightSpinBox) hl1.addSpacing(15) hl1.addWidget(QLabel("Padding:")) hl1.addSpacing(5) hl1.addWidget(self.paddingSpinBox) hl1.addSpacing(15) hl1.addWidget(self.forcePotCheckBox) hl1.addSpacing(15) hl1.addWidget(self.reorderTilesCheckBox) hl1.addStretch() hl2 = QHBoxLayout() hl2.setContentsMargins(5, 5, 5, 5) hl2.addWidget(self.transparentCheckbox) hl2.addSpacing(15) hl2.addWidget(self.backgroundColorLabel) hl2.addSpacing(5) hl2.addWidget(self.backgroundColorEdit) hl2.addStretch() hl3 = QHBoxLayout() hl3.setContentsMargins(5, 5, 5, 5) hl3.addWidget(self.generateAndExportButton) vl = QVBoxLayout() vl.setContentsMargins(0, 0, 0, 0) vl.setSpacing(0) vl.addLayout(hl1) vl.addLayout(hl2) vl.addWidget(self.pixmapWidget) vl.addLayout(hl3) w = QWidget() w.setLayout(vl) self.setCentralWidget(w) self.setTitle()
class Pqgit(QMainWindow): """ main class / entry point """ def __init__(self): super().__init__() self.setAttribute( Qt.WA_DeleteOnClose ) # let Qt delete stuff before the python garbage-collector gets to work self.repo = None self.branches_model = None # instantiate main window self.ui = ui.Ui_MainWindow() self.ui.setupUi(self) self.fs_watch = QFileSystemWatcher(self) self.fs_watch.fileChanged.connect(self.on_file_changed) self.fs_watch.directoryChanged.connect(self.on_dir_changed) self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'pqgit', 'config') # for comparison self.new_c_id, self.old_c_id = None, None # window icon cwd = os.path.dirname(os.path.realpath(__file__)) self.setWindowIcon(QIcon(os.path.join(cwd, 'Git-Icon-White.png'))) self.setWindowTitle('pqgit') # size and position self.move(self.settings.value('w/pos', QPoint(200, 200))) self.resize(self.settings.value('w/size', QSize(1000, 1000))) self.ui.hist_splitter.setSizes([ int(s) for s in self.settings.value('w/hist_splitter', [720, 360]) ]) self.ui.cinf_splitter.setSizes([ int(s) for s in self.settings.value('w/cinf_splitter', [360, 360]) ]) self.ui.diff_splitter.setSizes([ int(s) for s in self.settings.value('w/diff_splitter', [150, 1200, 230]) ]) # open repo dir open_shortcut = QShortcut(QKeySequence('Ctrl+O'), self) open_shortcut.activated.connect(self.open_dir) # set-up ui self.branches_model = BranchesModel() self.ui.tvBranches.setModel(self.branches_model) self.ui.tvBranches.selectionModel().selectionChanged.connect( self.branches_selection_changed) self.ui.tvBranches.resizeColumnsToContents() self.history_model = HistoryModel() self.ui.tvHistory.setModel(self.history_model) self.ui.tvHistory.selectionModel().selectionChanged.connect( self.history_selection_changed) self.files_model = FilesModel() self.ui.tvFiles.setModel(self.files_model) self.ui.tvFiles.selectionModel().selectionChanged.connect( self.files_selection_changed) self.ui.tvFiles.doubleClicked.connect(self.on_file_doubleclicked) for view in (self.ui.tvBranches, self.ui.tvHistory, self.ui.tvFiles): view.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) view.setSelectionBehavior(QAbstractItemView.SelectRows) view.setShowGrid(False) view.verticalHeader().setDefaultSectionSize( QApplication.font().pointSize() + 2) view.verticalHeader().hide() self.ui.teDiff.setFont(QFont('Monospace')) self.difftools = [] timer = QTimer(self) timer.timeout.connect(self.on_timer) timer.start(5000) self.dir_name = self.settings.value('last_opened_repo', None) try: pygit2.Repository(self.dir_name) except Exception: #pylint: disable=broad-except self.open_dir() return self.open_repo() def open_dir(self): """ show open dir dialog and open repo """ last_dir = self.settings.value('last_fileopen_dir', '') fd = QFileDialog(self, 'Open .git', last_dir) fd.setFileMode(QFileDialog.DirectoryOnly) fd.setFilter( QDir.Filters(QDir.Dirs | QDir.Hidden | QDir.NoDot | QDir.NoDotDot)) while True: if not fd.exec(): return self.dir_name = fd.selectedFiles()[0] parent = os.path.dirname(self.dir_name) self.settings.setValue('last_fileopen_dir', parent) self.settings.setValue('last_opened_repo', self.dir_name) try: pygit2.Repository(self.dir_name) break except pygit2.GitError: QMessageBox(self, text='Cannot open repo: ' + self.dir_name).exec() self.open_repo() def open_repo(self): """ called either on start or after open dialog """ self.setWindowTitle(f'{self.dir_name} - pqgit ({VERSION})') self.repo = pygit2.Repository(self.dir_name) # remove existing files and folder from watch if self.fs_watch.files(): self.fs_watch.removePaths(self.fs_watch.files()) if self.fs_watch.directories(): self.fs_watch.removePaths(self.fs_watch.directories()) wd = self.repo.workdir self.fs_watch.addPath(wd) # get head tree for list of files in repo target = self.repo.head.target last_commit = self.repo[target] tree_id = last_commit.tree_id tree = self.repo[tree_id] # add those files and folder to watch self.fs_watch.addPaths([wd + o[0] for o in parse_tree_rec(tree, True)]) # get files/folders not in repo from status self.fs_watch.addPaths([ wd + p for p, f in self.repo.status().items() if GIT_STATUS[f] != 'I' ]) # (doesn't matter some are in both lists, already monitored ones will not be added by Qt) # local branches branches = [] selected_branch_row = 0 for idx, b_str in enumerate(self.repo.branches.local): b = self.repo.branches[b_str] if b.is_checked_out(): selected_branch_row = idx branches.append( Branch(name=b.branch_name, ref=b.name, c_o=b.is_checked_out())) # tags regex = re.compile('^refs/tags') tags = list(filter(regex.match, self.repo.listall_references())) branches += [Branch(name=t[10:], ref=t, c_o=False) for t in tags] self.branches_model.update(branches) idx1 = self.branches_model.index(selected_branch_row, 0) idx2 = self.branches_model.index(selected_branch_row, self.branches_model.columnCount() - 1) self.ui.tvBranches.selectionModel().select(QItemSelection(idx1, idx2), QItemSelectionModel.Select) self.ui.tvHistory.resizeColumnsToContents() def on_timer(self): """ poll opened diff tools (like meld) and close temp files when finished """ for dt in self.difftools: if subprocess.Popen.poll(dt.proc) is not None: if dt.old_f: dt.old_f.close() if dt.new_f: dt.new_f.close() dt.running = False self.difftools[:] = [dt for dt in self.difftools if dt.running] def on_file_changed(self, path): """ existing files edited """ patch = self.files_model.patches[ self.ui.tvFiles.selectionModel().selectedRows()[0].row()] if self.repo.workdir + patch.path == path: self.files_selection_changed() def on_dir_changed(self, path): """ file added/deleted; refresh history to show it in 'working' """ # remember history selection history_ids = [] for idx in self.ui.tvHistory.selectionModel().selectedRows(): history_ids.append(self.history_model.commits[idx.row()].id) bak_path = self.files_model.patches[ self.ui.tvFiles.selectionModel().selectedRows()[0].row()].path self.refresh_history() # restore history selection for i in history_ids: for row, c in enumerate(self.history_model.commits): if c.id == i: idx1 = self.history_model.index(row, 0) idx2 = self.history_model.index( row, self.history_model.columnCount() - 1) self.ui.tvHistory.selectionModel().select( QItemSelection(idx1, idx2), QItemSelectionModel.Select) # restore file selection if not bak_path: return for row, patch in enumerate(self.files_model.patches): if patch.path == bak_path: idx1 = self.files_model.index(row, 0) idx2 = self.files_model.index( row, self.files_model.columnCount() - 1) self.ui.tvFiles.selectionModel().select( QItemSelection(idx1, idx2), QItemSelectionModel.Select) break def refresh_history(self): """ called and branch check-out (which is also called during start-up) to populate commit log """ commits = [] # working directory status = self.repo.status() if len(status.items()) > 0: commits.append(Commit('working', 'working', None, None, None, None)) for c in self.repo.walk(self.repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL): commit = Commit(id=c.id.hex, tree_id=c.tree_id.hex, author=c.author, dt=c.commit_time, dt_offs=c.commit_time_offset, message=c.message.strip()) commits.append(commit) self.history_model.update(commits) self.ui.tvHistory.resizeColumnsToContents() def branches_selection_changed(self): """ checkout selected branch """ selected_row = self.ui.tvBranches.selectionModel().selectedRows( )[0].row() self.repo.checkout(self.branches_model.branches[selected_row].ref, strategy=pygit2.GIT_CHECKOUT_SAFE) self.refresh_history() def on_file_doubleclicked(self, index): """ get files contents for revisions and start diff tool """ patch = self.files_model.patches[index.row()] if not patch.old_file_id: msg_box = QMessageBox(self) msg_box.setText("Nothing to compare to.") msg_box.exec() return old_f = tempfile.NamedTemporaryFile( prefix=f'old_{self.old_c_id[:7]}__') old_f.write(self.repo[patch.old_file_id].data) old_f.flush() new_f = None if patch.new_file_id: # compare 2 revisions new_f = tempfile.NamedTemporaryFile( prefix=f'new_{self.new_c_id[:7]}__') new_f.write(self.repo[patch.new_file_id].data) new_f.flush() new_f_name = new_f.name else: # compare some revision with working copy new_f_name = self.repo.workdir + patch.path.strip() proc = subprocess.Popen( [self.settings.value('diff_tool', 'meld'), old_f.name, new_f_name]) self.difftools.append(Proc(proc, old_f, new_f, True)) def history_selection_changed(self, selected): """ docstring """ self.ui.teDiff.setText('') self.new_c_id, self.old_c_id = None, None selection_model = self.ui.tvHistory.selectionModel() selected_rows = selection_model.selectedRows() self.ui.teCommit.setPlainText('') commit = None fst_tid, fst_obj = None, None snd_tid, snd_obj = None, None if len(selected_rows) < 1: # nothing to do return if len(selected_rows) > 2: # don't allow more than 2 selected lines selection_model.select(selected, QItemSelectionModel.Deselect) return if len(selected_rows) == 1: # single revision selected commit = self.history_model.commits[selected_rows[0].row()] fst_tid = commit.tree_id if selected_rows[0].row() + 1 < self.history_model.rowCount(): # there is a parent, get it's id to compare to it snd_commit = self.history_model.commits[selected_rows[0].row() + 1] snd_tid = snd_commit.tree_id self.new_c_id = commit.id self.old_c_id = snd_commit.id # set commit details in view if commit.tree_id != 'working': text = 'Commit: ' + commit.id + '\n\n' text += 'Author: ' + commit.author.name + ' <' + commit.author.email + '>\n\n' text += commit.message + '\n' self.ui.teCommit.setPlainText(text) else: # 2 revisions selected fst_row, snd_row = tuple( sorted([selected_rows[0].row(), selected_rows[1].row()])) commit = self.history_model.commits[fst_row] fst_tid = commit.tree_id snd_commit = self.history_model.commits[snd_row] snd_tid = snd_commit.tree_id self.new_c_id = commit.id self.old_c_id = snd_commit.id if fst_tid != 'working': fst_obj = self.repo.revparse_single(fst_tid) if snd_tid: snd_obj = self.repo.revparse_single(snd_tid) diff = None if fst_tid == 'working': # diff for working directory only shows... some files; get them anyway, then insert the ones from status diff = self.repo.diff( snd_obj, None) # regardless of snd_obj being something or None patches = [ Patch( p.delta.new_file.path.strip(), # p.delta.status_char(), None, # p.delta.new_file.id.hex is 'some' id, but it's somehow not ok... p.delta.old_file.id.hex if p.delta.old_file.id.hex.find('00000') < 0 else None, ) for p in diff ] inserted = [p.delta.new_file.path for p in diff] status = self.repo.status() for path, flags in status.items(): if path not in inserted: patches.append( Patch(path.strip(), GIT_STATUS[flags], None, None)) elif snd_obj: diff = self.repo.diff(snd_obj, fst_obj) patches = [ Patch( p.delta.new_file.path.strip(), # p.delta.status_char(), p.delta.new_file.id.hex if p.delta.new_file.id.hex.find('00000') < 0 else None, p.delta.old_file.id.hex if p.delta.old_file.id.hex.find('00000') < 0 else None, ) for p in diff ] else: # initial revision patches = [ Patch(o[0], 'A', o[1], None) for o in parse_tree_rec(fst_obj) ] patches = sorted(patches, key=lambda p: p.path) self.files_model.update(patches) self.ui.tvFiles.resizeColumnsToContents() def files_selection_changed(self): """ show diff (or file content for new, ignored, ... files) """ patch = self.files_model.patches[ self.ui.tvFiles.selectionModel().selectedRows()[0].row()] nf_data, of_data = None, None # new_file, old_file if patch.new_file_id: nf_data = self.repo[patch.new_file_id].data.decode('utf-8') if patch.old_file_id: of_data = self.repo[patch.old_file_id].data.decode('utf-8') if nf_data and of_data: html = _html_diff.make_file( fromlines=nf_data.splitlines(), # tolines=of_data.splitlines(), fromdesc=f'old ({self.old_c_id[:7]})', todesc=f'new ({self.new_c_id[:7]})', context=True) self.ui.teDiff.setHtml(html) elif nf_data: self.ui.teDiff.setText(nf_data) elif of_data: if patch.status == 'M': # this should be working directory compared to something else with open(self.repo.workdir + patch.path.strip()) as f: nf_data = f.read() html = _html_diff.make_file( fromlines=nf_data.splitlines(), tolines=of_data.splitlines(), fromdesc=f'old ({self.old_c_id[:7]})', todesc=f'new ({self.new_c_id[:7]})', context=True) self.ui.teDiff.setHtml(html) else: self.ui.teDiff.setText(of_data) else: with open(self.repo.workdir + patch.path.strip()) as f: self.ui.teDiff.setPlainText(f.read()) self.ui.diff_groupbox.setTitle( 'Diff' if nf_data and of_data else 'File') def closeEvent(self, event): # pylint: disable=invalid-name, no-self-use """ event handler for window closing; save settings """ del event self.settings.setValue('w/pos', self.pos()) self.settings.setValue('w/size', self.size()) self.settings.setValue('w/hist_splitter', self.ui.hist_splitter.sizes()) self.settings.setValue('w/cinf_splitter', self.ui.cinf_splitter.sizes()) self.settings.setValue('w/diff_splitter', self.ui.diff_splitter.sizes()) # delete any left temp files self.on_timer()
def set_up(self): self.data_dir_watcher = QFileSystemWatcher(self) if os.path.isdir(self.data_dir): self.data_dir_watcher.addPath(self.data_dir) self.data_dir_watcher.directoryChanged.connect(self.refresh)
class Loader(QWidget): def __init__(self, parent=None): super(Loader, self).__init__(parent=parent) self.ui = Ui_Loader() self.ui.setupUi(self) self.dir = QDir(QDir.currentPath() + '/programs/') self.dir.setFilter(QDir.Files or QDir.NoDotAndDotDot) self.fs_watcher = QFileSystemWatcher(self.dir.path()) self.fs_watcher.addPath(self.dir.path()) self.fs_watcher.directoryChanged.connect(self.update_program_list) self.send_status = QProgressDialog self.sender = Sender self.serialpropertiesvalues = \ { 'baudrate': Serial.BAUDRATES, 'parity': Serial.PARITIES, 'databits': Serial.BYTESIZES, 'stopbits': Serial.STOPBITS, 'flowcontrol': ['NoControl', 'SoftwareControl', 'HardwareControl'] } self.update_program_list() self.update_serial_port_list() # self.set_serial_port_options() self.ui.updateProgramListButton.clicked.connect(self.refresh) self.ui.programListWidget.itemSelectionChanged.connect( self.selection_changed) self.ui.sendButton.clicked.connect(self.send_program) self.ui.serialPortChooser.currentTextChanged.connect( self.selection_changed) self.ui.serialPortChooser.currentTextChanged.connect(save_port) # self.ui.baudRateInput.textChanged.connect(save_baud) # self.ui.parityChooser.currentTextChanged.connect(save_parity) # self.ui.dataBitsChooser.currentTextChanged.connect(save_databits) # self.ui.stopBitsChooser.currentTextChanged.connect(save_stopbits) # self.ui.flowControlChooser.currentTextChanged.connect(save_flowcontrol) self.thread_pool = QThreadPool() def set_serial_port_options(self): for key in parities.keys(): self.ui.parityChooser.addItem(key) for key in bytesize.keys(): self.ui.dataBitsChooser.addItem(key) for key in stopbits.keys(): self.ui.stopBitsChooser.addItem(key) self.ui.flowControlChooser.addItems(flowcontrol) if globalSettings.contains('serialport/port'): self.selectpreviousvalues() else: self.saveconfig() def selectpreviousvalues(self): self.ui.serialPortChooser.setCurrentText( globalSettings.value('serialport/port')) self.ui.baudRateInput.setText( globalSettings.value('serialport/baudrate')) self.ui.parityChooser.setCurrentText( globalSettings.value('serialport/parity')) self.ui.dataBitsChooser.setCurrentText( globalSettings.value('serialport/databits')) self.ui.stopBitsChooser.setCurrentText( globalSettings.value('serialport/stopbits')) self.ui.flowControlChooser.setCurrentText( globalSettings.value('serialport/flowcontrol')) def saveconfig(self): save_port(self.ui.serialPortChooser.currentText()) save_baud(self.ui.baudRateInput.text()) save_parity(self.ui.parityChooser.currentText()) save_databits(self.ui.dataBitsChooser.currentText()) save_stopbits(self.ui.stopBitsChooser.currentText()) save_flowcontrol(self.ui.flowControlChooser.currentText()) def update_serial_port_list(self): self.ui.serialPortChooser.clear() for port in list_ports.comports(): self.ui.serialPortChooser.addItem(port.device) def update_program_list(self): self.ui.programListWidget.clear() self.dir.refresh() self.ui.programListWidget.addItems(self.dir.entryList()) self.ui.programListWidget.clearSelection() def selection_changed(self): if self.ui.serialPortChooser.currentText() is not None \ and self.ui.programListWidget.currentItem() is not None: self.ui.sendButton.setEnabled(True) else: self.ui.sendButton.setDisabled(True) def refresh(self): self.update_program_list() self.update_serial_port_list() def send_program(self): selections = self.ui.programListWidget.selectedItems() for selection in selections: filename = selection.text() filepath = self.dir.path() + '/' + filename port_chosen = self.ui.serialPortChooser.currentText() confirm = ConfirmSend(self) confirm.ui.dialogLabel.setText(f'Send program \'{filename}\'?') confirm.exec() if confirm.result() == QDialog.Accepted: self.send_status = QProgressDialog(self) self.sender = Sender( port_chosen, filepath, globalSettings.value('serialport/baudrate'), globalSettings.value('serialport/databits'), globalSettings.value('serialport/parity'), globalSettings.value('serialport/stopbits'), globalSettings.value('serialport/flowcontrol'), self) self.send_status.setMaximum(self.sender.file.size()) self.send_status.canceled.connect(self.sender.cancel) self.sender.signals.update_status.connect( self.send_status.setValue) self.thread_pool.start(self.sender) self.send_status.exec_() self.send_status.deleteLater()