class FileWatcher(QObject): pathChanged = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._watcher = QFileSystemWatcher(self) self._watcher.directoryChanged.connect(self.onChanged) self._watcher.fileChanged.connect(self.onChanged) self._events = {} @property def watched(self): return self._watcher.directories() + self._watcher.files() def clear(self): self._watcher.removePaths(self.watched) def watch(self, path): self._watcher.addPath(path) def watchWalk(self, path: Path): try: for root, dirs, files in os.walk(str(path)): for dir in dirs: r = Path(root).joinpath(dir) self._watcher.addPath(str(r)) except Exception: pass def onChanged(self, path): self.pathChanged.emit(path)
class FileWatcher(QObject): def __init__(self, ui, path): QObject.__init__(self) self._watcher = QFileSystemWatcher() self.ui = ui self._path = path self.set_watcher_path(self._path) def enable(self): self._watcher.fileChanged.connect(self._onFileChanged) def disable(self): self._watcher.fileChanged.disconnect(self._onFileChanged) def set_watcher_path(self, path): if self._watcher.files(): self._watcher.removePaths(self._watcher.files()) if path is not None and os.path.isfile(path): self._watcher.addPath(path) self._path = path @pyqtSlot() def _onFileChanged(self): if os.path.exists(self._path): # Get last line line = self.last_insert(self._path).decode('utf-8')[:-1] # Split datetime and event output and raise an event line = line.split(' ', 1) event = Event(str(datetime.datetime.now()), line[1]) self.addNotification(event) def last_insert(self, path): return subprocess.check_output(['tail', '-1', path]) def addNotification(self, event): def eventColor(details): if details.startswith('Warning'): return QColor(192, 192, 192) elif details.startswith('Error'): return QColor(255, 204, 204) elif details.startswith('Notice'): return QColor(255, 204, 229) elif details.startswith('Emergency'): return QColor(255, 102, 102) elif details.startswith('Informational'): return QColor(255, 229, 204) else: return QColor(255, 255, 255) values = [event.datetime, event.title, event.filename, event.details] self.ui.notificationsTableWidget.insertRow(0) for i in range(self.ui.notificationsTableWidget.columnCount()): rowItem = QTableWidgetItem(values[i]) rowItem.setBackground(eventColor(event.details)) self.ui.notificationsTableWidget.setItem(0, i, rowItem)
class FileWatcher(object): def __init__(self, files=None): self._watcher = QFileSystemWatcher() self._files = list() # List of file(s) to watch self.isWatching = False if files is not None: self.addFile(files) def test(self): file = ['/Users/bitzer/hudat.spec'] self.addFile(file) def addFile(self, files): # Add a file(s) to the watch list # Files needs to be a list, even if a single file # Do it while actively watching? for _f in files: print(_f) self._files.append(_f) def removeFile(self): # Remove a file pass def replaceFile(self, file): # In the (usual) case of a watching a single file, replace it if len(self._files) != 1: print('Only allowed if one file is currently watched') self._files[0] = file def startWatch(self): # Start watching the files self._watcher.addPaths(self._files) self._watcher.fileChanged.connect(self.onChange) self.isWatching = True def stopWatch(self): # Stop Watching the folder self._watcher.removePaths(self._files) self._watcher.fileChanged.disconnect(self.onChange) self.isWatching = False def onChange(self, file): # When a file changes, do something # Which file changed? print('changed ' + file)
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.centralwidget.hide() self.project = ProjectManager() self.modules_manager = ModuleManager(self) self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North) self.modules_manager.init_modules([ CompilerModule, LevelWidget, AssetViewWidget, AssetBrowser, LogWidget, ProfilerWidget, ScriptEditorManager ]) self.file_watch = QFileSystemWatcher(self) self.file_watch.fileChanged.connect(self.file_changed) self.file_watch.directoryChanged.connect(self.dir_changed) self.build_file_watch = QFileSystemWatcher(self) self.build_file_watch.fileChanged.connect(self.build_file_changed) self.build_file_watch.directoryChanged.connect(self.build_dir_changed) def open_project(self, name, dir): self.project.open_project(name, dir) self.modules_manager.open_project(self.project) self.watch_project_dir() def reload_all(self): self.modules_manager['compiler'].compile_all() for k, v in self.project.instances.items(): v.console_api.reload_all() def watch_project_dir(self): files = self.file_watch.files() directories = self.file_watch.directories() if len(files): self.file_watch.removePaths(files) if len(directories): self.file_watch.removePaths(directories) files = self.build_file_watch.files() directories = self.build_file_watch.directories() if len(files): self.build_file_watch.removePaths(files) if len(directories): self.build_file_watch.removePaths(directories) files = [] it = QDirIterator(self.project.source_dir, QDirIterator.Subdirectories) while it.hasNext(): files.append(it.next()) self.file_watch.addPaths(files) files = [] it = QDirIterator(self.project.build_dir, QDirIterator.Subdirectories) while it.hasNext(): files.append(it.next()) self.build_file_watch.addPaths(files) def file_changed(self, path): self.modules_manager['compiler'].compile_all() def dir_changed(self, path): self.watch_project_dir() def build_file_changed(self, path): pass def build_dir_changed(self, path): pass def open_script_editor(self): self.script_editor_dock_widget.show() def open_recorded_events(self): self.modules_manager['profiler'].dock.show() def run_standalone(self): self.project.run_release("Standalone") def run_level(self): self.project.run_develop("Level", compile_=True, continue_=True, port=5566) def closeEvent(self, evnt): self.modules_manager.close_project() self.project.killall() evnt.accept()
class ExecuteOptionsPlugin(QWidget, Plugin): """ Handles setting the various arguments for running. Signals: executableChanged(str): Path of the new executable is emitted when changed executableInfoChanged(ExecutableInfo): Emitted when the executable path is changed workingDirChanged(str): Path of the current directory is changed """ executableChanged = pyqtSignal(str) executableInfoChanged = pyqtSignal(ExecutableInfo) workingDirChanged = pyqtSignal(str) def __init__(self, **kwds): super(ExecuteOptionsPlugin, self).__init__(**kwds) self._preferences.addInt("execute/maxRecentWorkingDirs", "Max recent working directories", 10, 1, 50, "Set the maximum number of recent working directories that have been used.", ) self._preferences.addInt("execute/maxRecentExes", "Max recent executables", 10, 1, 50, "Set the maximum number of recent executables that have been used.", ) self._preferences.addInt("execute/maxRecentArgs", "Max recent command line arguments", 10, 1, 50, "Set the maximum number of recent command line arguments that have been used.", ) self._preferences.addBool("execute/mpiEnabled", "Enable MPI by default", False, "Set the MPI checkbox on by default", ) self._preferences.addString("execute/mpiArgs", "Default mpi command", "mpiexec -n 2", "Set the default MPI command to run", ) self._preferences.addBool("execute/threadsEnabled", "Enable threads by default", False, "Set the threads checkbox on by default", ) self._preferences.addString("execute/threadsArgs", "Default threads arguments", "--n-threads=2", "Set the default threads arguments", ) self.all_exe_layout = WidgetUtils.addLayout(grid=True) self.setLayout(self.all_exe_layout) self.working_label = WidgetUtils.addLabel(None, self, "Working Directory") self.all_exe_layout.addWidget(self.working_label, 0, 0) self.choose_working_button = WidgetUtils.addButton(None, self, "Choose", self._chooseWorkingDir) self.all_exe_layout.addWidget(self.choose_working_button, 0, 1) self.working_line = WidgetUtils.addLineEdit(None, self, None, readonly=True) self.working_line.setText(os.getcwd()) self.all_exe_layout.addWidget(self.working_line, 0, 2) self.exe_label = WidgetUtils.addLabel(None, self, "Executable") self.all_exe_layout.addWidget(self.exe_label, 1, 0) self.choose_exe_button = WidgetUtils.addButton(None, self, "Choose", self._chooseExecutable) self.all_exe_layout.addWidget(self.choose_exe_button, 1, 1) self.exe_line = WidgetUtils.addLineEdit(None, self, None, readonly=True) self.all_exe_layout.addWidget(self.exe_line, 1, 2) self.args_label = WidgetUtils.addLabel(None, self, "Extra Arguments") self.all_exe_layout.addWidget(self.args_label, 2, 0) self.args_line = WidgetUtils.addLineEdit(None, self, None) self.all_exe_layout.addWidget(self.args_line, 2, 2) self.mpi_label = WidgetUtils.addLabel(None, self, "Use MPI") self.all_exe_layout.addWidget(self.mpi_label, 3, 0) self.mpi_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.mpi_checkbox.setChecked(self._preferences.value("execute/mpiEnabled")) self.all_exe_layout.addWidget(self.mpi_checkbox, 3, 1, alignment=Qt.AlignHCenter) self.mpi_line = WidgetUtils.addLineEdit(None, self, None) self.mpi_line.setText(self._preferences.value("execute/mpiArgs")) self.mpi_line.cursorPositionChanged.connect(self._mpiLineCursorChanged) self.all_exe_layout.addWidget(self.mpi_line, 3, 2) self.threads_label = WidgetUtils.addLabel(None, self, "Use Threads") self.all_exe_layout.addWidget(self.threads_label, 4, 0) self.threads_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.threads_checkbox.setChecked(self._preferences.value("execute/threadsEnabled")) self.all_exe_layout.addWidget(self.threads_checkbox, 4, 1, alignment=Qt.AlignHCenter) self.threads_line = WidgetUtils.addLineEdit(None, self, None) self.threads_line.setText(self._preferences.value("execute/threadsArgs")) self.threads_line.cursorPositionChanged.connect(self._threadsLineCursorChanged) self.all_exe_layout.addWidget(self.threads_line, 4, 2) self.csv_label = WidgetUtils.addLabel(None, self, "Postprocessor CSV Output") self.all_exe_layout.addWidget(self.csv_label, 5, 0) self.csv_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.all_exe_layout.addWidget(self.csv_checkbox, 5, 1, alignment=Qt.AlignHCenter) self.csv_checkbox.setCheckState(Qt.Checked) self.recover_label = WidgetUtils.addLabel(None, self, "Recover") self.all_exe_layout.addWidget(self.recover_label, 6, 0) self.recover_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.all_exe_layout.addWidget(self.recover_checkbox, 6, 1, alignment=Qt.AlignHCenter) self._recent_exe_menu = None self._recent_working_menu = None self._recent_args_menu = None self._exe_watcher = QFileSystemWatcher() self._exe_watcher.fileChanged.connect(self.setExecutablePath) self._loading_dialog = QMessageBox(parent=self) self._loading_dialog.setWindowTitle("Loading executable") self._loading_dialog.setStandardButtons(QMessageBox.NoButton) # get rid of the OK button self._loading_dialog.setWindowModality(Qt.ApplicationModal) self._loading_dialog.setIcon(QMessageBox.Information) self._loading_dialog.setText("Loading executable") self.setup() def setExecutablePath(self, app_path): """ The user select a new executable path. Input: app_path: The path of the executable. """ if not app_path: return self._loading_dialog.setInformativeText(app_path) self._loading_dialog.show() self._loading_dialog.raise_() QApplication.processEvents() app_info = ExecutableInfo() app_info.setPath(app_path) QApplication.processEvents() if app_info.valid(): self.exe_line.setText(app_path) self.executableInfoChanged.emit(app_info) self.executableChanged.emit(app_path) files = self._exe_watcher.files() if files: self._exe_watcher.removePaths(files) self._exe_watcher.addPath(app_path) self._updateRecentExe(app_path, not app_info.valid()) self._loading_dialog.hide() def _chooseExecutable(self): """ Open a dialog to allow the user to choose an executable. """ #FIXME: QFileDialog seems to be a bit broken. Using # .setFilter() to filter only executable files doesn't # seem to work. Setting a QSortFilterProxyModel doesn't # seem to work either. # So just use the static method. exe_name, other = QFileDialog.getOpenFileName(self, "Chooose executable") self.setExecutablePath(exe_name) def _workingDirChanged(self): """ Slot called when working directory changed. """ working = str(self.working_line.text()) self.setWorkingDir(working) def _chooseWorkingDir(self): """ Open dialog to choose a current working directory. """ dirname = QFileDialog.getExistingDirectory(self, "Choose directory") self.setWorkingDir(dirname) def setWorkingDir(self, dir_name): """ Sets the working directory. Input: dir_name: The path of the working directory. """ if not dir_name: return old_dirname = str(self.working_line.text()) try: os.chdir(dir_name) self.working_line.setText(dir_name) if old_dirname != dir_name: self.workingDirChanged.emit(dir_name) self._updateRecentWorkingDir(dir_name) except OSError: mooseutils.mooseError("Invalid directory %s" % dir_name, dialog=True) self._updateRecentWorkingDir(dir_name, True) def _setExecutableArgs(self, args): """ Set the executable arguments. Input: args: str: A string of all the arguments. """ self.args_line.setText(args) def buildCommand(self, input_file): cmd, args = self.buildCommandWithNoInputFile() args.append("-i") args.append(os.path.relpath(input_file)) return cmd, args def buildCommandWithNoInputFile(self): """ Builds the full command line with arguments. Return: <string of command to run>, <list of arguments> """ cmd = "" args = [] if self.mpi_checkbox.isChecked(): mpi_args = shlex.split(str(self.mpi_line.text())) if mpi_args: cmd = mpi_args[0] args = mpi_args[1:] args.append(str(self.exe_line.text())) if not cmd: cmd = str(self.exe_line.text()) args += shlex.split(str(self.args_line.text())) if self.recover_checkbox.isChecked(): args.append("--recover") if self.csv_checkbox.isChecked(): #args.append("--no-color") args.append("Outputs/csv=true") if self.threads_checkbox.isChecked(): args += shlex.split(str(self.threads_line.text())) return cmd, args def _updateRecentExe(self, path, remove=False): """ Updates the recently used menu with the current executable """ if self._recent_exe_menu: abs_path = os.path.normcase(os.path.abspath(path)) if remove: self._recent_exe_menu.removeEntry(abs_path) else: self._recent_exe_menu.update(abs_path) def _updateRecentWorkingDir(self, path, remove=False): """ Updates the recently used menu with the current executable """ full_path = os.path.abspath(path) if self._recent_working_menu: if remove: self._recent_working_menu.removeEntry(full_path) else: self._recent_working_menu.update(full_path) def onPreferencesSaved(self): self._recent_args_menu.updateRecentlyOpened() self._recent_working_menu.updateRecentlyOpened() self._recent_exe_menu.updateRecentlyOpened() def addToMenu(self, menu): """ Adds menu entries specific to the Arguments to the menubar. """ workingMenu = menu.addMenu("Recent &working dirs") self._recent_working_menu = RecentlyUsedMenu(workingMenu, "execute/recentWorkingDirs", "execute/maxRecentWorkingDirs", 20, ) self._recent_working_menu.selected.connect(self.setWorkingDir) self._workingDirChanged() exeMenu = menu.addMenu("Recent &executables") self._recent_exe_menu = RecentlyUsedMenu(exeMenu, "execute/recentExes", "execute/maxRecentExes", 20, ) self._recent_exe_menu.selected.connect(self.setExecutablePath) argsMenu = menu.addMenu("Recent &arguments") self._recent_args_menu = RecentlyUsedMenu(argsMenu, "execute/recentArgs", "execute/maxRecentArgs", 20, ) self._recent_args_menu.selected.connect(self._setExecutableArgs) def clearRecentlyUsed(self): if self._recent_args_menu: self._recent_args_menu.clearValues() self._recent_working_menu.clearValues() self._recent_exe_menu.clearValues() self._workingDirChanged() def _mpiLineCursorChanged(self, old, new): self.mpi_checkbox.setChecked(True) def _threadsLineCursorChanged(self, old, new): self.threads_checkbox.setChecked(True)
class comics_project_manager_docker(DockWidget): setupDictionary = {} stringName = i18n("Comics Manager") projecturl = None pagesWatcher = None def __init__(self): super().__init__() self.setWindowTitle(self.stringName) # Setup layout: base = QHBoxLayout() widget = QWidget() widget.setLayout(base) baseLayout = QSplitter() base.addWidget(baseLayout) self.setWidget(widget) buttonLayout = QVBoxLayout() buttonBox = QWidget() buttonBox.setLayout(buttonLayout) baseLayout.addWidget(buttonBox) # Comic page list and pages model self.comicPageList = QListView() self.comicPageList.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.comicPageList.setDragEnabled(True) self.comicPageList.setDragDropMode(QAbstractItemView.InternalMove) self.comicPageList.setDefaultDropAction(Qt.MoveAction) self.comicPageList.setAcceptDrops(True) self.comicPageList.setItemDelegate(comic_page_delegate()) self.pagesModel = QStandardItemModel() self.comicPageList.doubleClicked.connect(self.slot_open_page) self.comicPageList.setIconSize(QSize(128, 128)) # self.comicPageList.itemDelegate().closeEditor.connect(self.slot_write_description) self.pagesModel.layoutChanged.connect(self.slot_write_config) self.pagesModel.rowsInserted.connect(self.slot_write_config) self.pagesModel.rowsRemoved.connect(self.slot_write_config) self.pagesModel.rowsMoved.connect(self.slot_write_config) self.comicPageList.setModel(self.pagesModel) pageBox = QWidget() pageBox.setLayout(QVBoxLayout()) zoomSlider = QSlider(Qt.Horizontal, None) zoomSlider.setRange(1, 8) zoomSlider.setValue(4) zoomSlider.setTickInterval(1) zoomSlider.setMinimumWidth(10) zoomSlider.valueChanged.connect(self.slot_scale_thumbnails) self.projectName = Elided_Text_Label() pageBox.layout().addWidget(self.projectName) pageBox.layout().addWidget(zoomSlider) pageBox.layout().addWidget(self.comicPageList) baseLayout.addWidget(pageBox) self.btn_project = QToolButton() self.btn_project.setPopupMode(QToolButton.MenuButtonPopup) self.btn_project.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) menu_project = QMenu() self.action_new_project = QAction(i18n("New Project"), self) self.action_new_project.triggered.connect(self.slot_new_project) self.action_load_project = QAction(i18n("Open Project"), self) self.action_load_project.triggered.connect(self.slot_open_config) menu_project.addAction(self.action_new_project) menu_project.addAction(self.action_load_project) self.btn_project.setMenu(menu_project) self.btn_project.setDefaultAction(self.action_load_project) buttonLayout.addWidget(self.btn_project) # Settings dropdown with actions for the different settings menus. self.btn_settings = QToolButton() self.btn_settings.setPopupMode(QToolButton.MenuButtonPopup) self.btn_settings.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_edit_project_settings = QAction(i18n("Project Settings"), self) self.action_edit_project_settings.triggered.connect(self.slot_edit_project_settings) self.action_edit_meta_data = QAction(i18n("Meta Data"), self) self.action_edit_meta_data.triggered.connect(self.slot_edit_meta_data) self.action_edit_export_settings = QAction(i18n("Export Settings"), self) self.action_edit_export_settings.triggered.connect(self.slot_edit_export_settings) menu_settings = QMenu() menu_settings.addAction(self.action_edit_project_settings) menu_settings.addAction(self.action_edit_meta_data) menu_settings.addAction(self.action_edit_export_settings) self.btn_settings.setDefaultAction(self.action_edit_project_settings) self.btn_settings.setMenu(menu_settings) buttonLayout.addWidget(self.btn_settings) self.btn_settings.setDisabled(True) # Add page drop down with different page actions. self.btn_add_page = QToolButton() self.btn_add_page.setPopupMode(QToolButton.MenuButtonPopup) self.btn_add_page.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_add_page = QAction(i18n("Add Page"), self) self.action_add_page.triggered.connect(self.slot_add_new_page_single) self.action_add_template = QAction(i18n("Add Page from Template"), self) self.action_add_template.triggered.connect(self.slot_add_new_page_from_template) self.action_add_existing = QAction(i18n("Add Existing Pages"), self) self.action_add_existing.triggered.connect(self.slot_add_page_from_url) self.action_remove_selected_page = QAction(i18n("Remove Page"), self) self.action_remove_selected_page.triggered.connect(self.slot_remove_selected_page) self.action_resize_all_pages = QAction(i18n("Batch Resize"), self) self.action_resize_all_pages.triggered.connect(self.slot_batch_resize) self.btn_add_page.setDefaultAction(self.action_add_page) self.action_show_page_viewer = QAction(i18n("View Page In Window"), self) self.action_show_page_viewer.triggered.connect(self.slot_show_page_viewer) self.action_scrape_authors = QAction(i18n("Scrape Author Info"), self) self.action_scrape_authors.setToolTip(i18n("Search for author information in documents and add it to the author list. This does not check for duplicates.")) self.action_scrape_authors.triggered.connect(self.slot_scrape_author_list) self.action_scrape_translations = QAction(i18n("Scrape Text for Translation"), self) self.action_scrape_translations.triggered.connect(self.slot_scrape_translations) actionList = [] menu_page = QMenu() actionList.append(self.action_add_page) actionList.append(self.action_add_template) actionList.append(self.action_add_existing) actionList.append(self.action_remove_selected_page) actionList.append(self.action_resize_all_pages) actionList.append(self.action_show_page_viewer) actionList.append(self.action_scrape_authors) actionList.append(self.action_scrape_translations) menu_page.addActions(actionList) self.btn_add_page.setMenu(menu_page) buttonLayout.addWidget(self.btn_add_page) self.btn_add_page.setDisabled(True) self.comicPageList.setContextMenuPolicy(Qt.ActionsContextMenu) self.comicPageList.addActions(actionList) # Export button that... exports. self.btn_export = QPushButton(i18n("Export Comic")) self.btn_export.clicked.connect(self.slot_export) buttonLayout.addWidget(self.btn_export) self.btn_export.setDisabled(True) self.btn_project_url = QPushButton(i18n("Copy Location")) self.btn_project_url.setToolTip(i18n("Copies the path of the project to the clipboard. Useful for quickly copying to a file manager or the like.")) self.btn_project_url.clicked.connect(self.slot_copy_project_url) self.btn_project_url.setDisabled(True) buttonLayout.addWidget(self.btn_project_url) self.page_viewer_dialog = comics_project_page_viewer.comics_project_page_viewer() self.pagesWatcher = QFileSystemWatcher() self.pagesWatcher.fileChanged.connect(self.slot_check_for_page_update) buttonLayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) """ Open the config file and load the json file into a dictionary. """ def slot_open_config(self): self.path_to_config = QFileDialog.getOpenFileName(caption=i18n("Please select the JSON comic config file."), filter=str(i18n("JSON files") + "(*.json)"))[0] if os.path.exists(self.path_to_config) is True: if os.access(self.path_to_config, os.W_OK) is False: QMessageBox.warning(None, i18n("Config cannot be used"), i18n("Krita doesn't have write access to this folder, so new files cannot be made. Please configure the folder access or move the project to a folder that can be written to."), QMessageBox.Ok) return configFile = open(self.path_to_config, "r", newline="", encoding="utf-16") self.setupDictionary = json.load(configFile) self.projecturl = os.path.dirname(str(self.path_to_config)) configFile.close() self.load_config() """ Further config loading. """ def load_config(self): self.projectName.setMainText(text=str(self.setupDictionary["projectName"])) self.fill_pages() self.btn_settings.setEnabled(True) self.btn_add_page.setEnabled(True) self.btn_export.setEnabled(True) self.btn_project_url.setEnabled(True) """ Fill the pages model with the pages from the pages list. """ def fill_pages(self): self.loadingPages = True self.pagesModel.clear() if len(self.pagesWatcher.files())>0: self.pagesWatcher.removePaths(self.pagesWatcher.files()) pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] progress = QProgressDialog() progress.setMinimum(0) progress.setMaximum(len(pagesList)) progress.setWindowTitle(i18n("Loading Pages...")) for url in pagesList: absurl = os.path.join(self.projecturl, url) relative = os.path.relpath(absurl, self.projecturl) if (os.path.exists(absurl)): #page = Application.openDocument(absurl) page = zipfile.ZipFile(absurl, "r") thumbnail = QImage.fromData(page.read("preview.png")) pageItem = QStandardItem() dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) pageItem.setText(dataList[0].replace("_", " ")) pageItem.setDragEnabled(True) pageItem.setDropEnabled(False) pageItem.setEditable(False) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setData(dataList[1], role = CPE.DESCRIPTION) pageItem.setData(relative, role = CPE.URL) self.pagesWatcher.addPath(absurl) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) pageItem.setToolTip(relative) page.close() self.pagesModel.appendRow(pageItem) progress.setValue(progress.value() + 1) progress.setValue(len(pagesList)) self.loadingPages = False """ Function that is triggered by the zoomSlider Resizes the thumbnails. """ def slot_scale_thumbnails(self, multiplier=4): self.comicPageList.setIconSize(QSize(multiplier * 32, multiplier * 32)) """ Function that takes the documentinfo.xml and parses it for the title, subject and abstract tags, to get the title and description. @returns a stringlist with the name on 0 and the description on 1. """ def get_description_and_title(self, string): xmlDoc = ET.fromstring(string) calligra = str("{http://www.calligra.org/DTD/document-info}") name = "" if ET.iselement(xmlDoc[0].find(calligra + 'title')): name = xmlDoc[0].find(calligra + 'title').text if name is None: name = " " desc = "" if ET.iselement(xmlDoc[0].find(calligra + 'subject')): desc = xmlDoc[0].find(calligra + 'subject').text if desc is None or desc.isspace() or len(desc) < 1: if ET.iselement(xmlDoc[0].find(calligra + 'abstract')): desc = xmlDoc[0].find(calligra + 'abstract').text if desc is not None: if desc.startswith("<![CDATA["): desc = desc[len("<![CDATA["):] if desc.startswith("]]>"): desc = desc[:-len("]]>")] keywords = "" if ET.iselement(xmlDoc[0].find(calligra + 'keyword')): keywords = xmlDoc[0].find(calligra + 'keyword').text date = "" if ET.iselement(xmlDoc[0].find(calligra + 'date')): date = xmlDoc[0].find(calligra + 'date').text author = [] if ET.iselement(xmlDoc[1].find(calligra + 'creator-first-name')): string = xmlDoc[1].find(calligra + 'creator-first-name').text if string is not None: author.append(string) if ET.iselement(xmlDoc[1].find(calligra + 'creator-last-name')): string = xmlDoc[1].find(calligra + 'creator-last-name').text if string is not None: author.append(string) if ET.iselement(xmlDoc[1].find(calligra + 'full-name')): string = xmlDoc[1].find(calligra + 'full-name').text if string is not None: author.append(string) return [name, desc, keywords, date, " ".join(author)] """ Scrapes authors from the author data in the document info and puts them into the author list. Doesn't check for duplicates. """ def slot_scrape_author_list(self): listOfAuthors = [] if "authorList" in self.setupDictionary.keys(): listOfAuthors = self.setupDictionary["authorList"] if "pages" in self.setupDictionary.keys(): for relurl in self.setupDictionary["pages"]: absurl = os.path.join(self.projecturl, relurl) page = zipfile.ZipFile(absurl, "r") xmlDoc = ET.fromstring(page.read("documentinfo.xml")) calligra = str("{http://www.calligra.org/DTD/document-info}") authorelem = xmlDoc.find(calligra + 'author') author = {} if ET.iselement(authorelem.find(calligra + 'full-name')): author["nickname"] = str(authorelem.find(calligra + 'full-name').text) if ET.iselement(authorelem.find(calligra + 'creator-first-name')): author["first-name"] = str(authorelem.find(calligra + 'creator-first-name').text) if ET.iselement(authorelem.find(calligra + 'initial')): author["initials"] = str(authorelem.find(calligra + 'initial').text) if ET.iselement(authorelem.find(calligra + 'creator-last-name')): author["last-name"] = str(authorelem.find(calligra + 'creator-last-name').text) if ET.iselement(authorelem.find(calligra + 'email')): author["email"] = str(authorelem.find(calligra + 'email').text) if ET.iselement(authorelem.find(calligra + 'contact')): contact = authorelem.find(calligra + 'contact') contactMode = contact.get("type") if contactMode == "email": author["email"] = str(contact.text) if contactMode == "homepage": author["homepage"] = str(contact.text) if ET.iselement(authorelem.find(calligra + 'position')): author["role"] = str(authorelem.find(calligra + 'position').text) listOfAuthors.append(author) page.close() self.setupDictionary["authorList"] = listOfAuthors """ Edit the general project settings like the project name, concept, pages location, export location, template location, metadata """ def slot_edit_project_settings(self): dialog = comics_project_settings_dialog.comics_project_details_editor(self.projecturl) dialog.setConfig(self.setupDictionary, self.projecturl) if dialog.exec_() == QDialog.Accepted: self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() self.projectName.setMainText(str(self.setupDictionary["projectName"])) """ This allows users to select existing pages and add them to the pages list. The pages are currently not copied to the pages folder. Useful for existing projects. """ def slot_add_page_from_url(self): # get the pages. urlList = QFileDialog.getOpenFileNames(caption=i18n("Which existing pages to add?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0] # get the existing pages list. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] # And add each url in the url list to the pages list and the model. for url in urlList: if self.projecturl not in urlList: newUrl = os.path.join(self.projecturl, self.setupDictionary["pagesLocation"], os.path.basename(url)) shutil.move(url, newUrl) url = newUrl relative = os.path.relpath(url, self.projecturl) if url not in pagesList: page = zipfile.ZipFile(url, "r") thumbnail = QImage.fromData(page.read("preview.png")) dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(dataList[0].replace("_", " ")) newPageItem.setData(dataList[1], role = CPE.DESCRIPTION) newPageItem.setData(relative, role = CPE.URL) self.pagesWatcher.addPath(url) newPageItem.setData(dataList[2], role = CPE.KEYWORDS) newPageItem.setData(dataList[3], role = CPE.LASTEDIT) newPageItem.setData(dataList[4], role = CPE.EDITOR) newPageItem.setToolTip(relative) page.close() self.pagesModel.appendRow(newPageItem) """ Remove the selected page from the list of pages. This does not remove it from disk(far too dangerous). """ def slot_remove_selected_page(self): index = self.comicPageList.currentIndex() self.pagesModel.removeRow(index.row()) """ This function adds a new page from the default template. If there's no default template, or the file does not exist, it will show the create/import template dialog. It will remember the selected item as the default template. """ def slot_add_new_page_single(self): templateUrl = "templatepage" templateExists = False if "singlePageTemplate" in self.setupDictionary.keys(): templateUrl = self.setupDictionary["singlePageTemplate"] if os.path.exists(os.path.join(self.projecturl, templateUrl)): templateExists = True if templateExists is False: if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.setupDictionary["singlePageTemplate"] = templateUrl if os.path.exists(os.path.join(self.projecturl, templateUrl)): self.add_new_page(templateUrl) """ This function always asks for a template showing the new template window. This allows users to have multiple different templates created for back covers, spreads, other and have them accessible, while still having the convenience of a singular "add page" that adds a default. """ def slot_add_new_page_from_template(self): if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.add_new_page(templateUrl) """ This is the actual function that adds the template using the template url. It will attempt to name the new page projectName+number. """ def add_new_page(self, templateUrl): # check for page list and or location. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] if not "pageNumber" in self.setupDictionary.keys(): self.setupDictionary['pageNumber'] = 0 if (str(self.setupDictionary["pagesLocation"]).isspace()): self.setupDictionary["pagesLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where should the pages go?"), options=QFileDialog.ShowDirsOnly), self.projecturl) # Search for the possible name. extraUnderscore = str() if str(self.setupDictionary["projectName"])[-1].isdigit(): extraUnderscore = "_" self.setupDictionary['pageNumber'] += 1 pageName = str(self.setupDictionary["projectName"]).replace(" ", "_") + extraUnderscore + str(format(self.setupDictionary['pageNumber'], "03d")) url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra") # open the page by opening the template and resaving it, or just opening it. absoluteUrl = os.path.join(self.projecturl, url) if (os.path.exists(absoluteUrl)): newPage = Application.openDocument(absoluteUrl) else: booltemplateExists = os.path.exists(os.path.join(self.projecturl, templateUrl)) if booltemplateExists is False: templateUrl = os.path.relpath(QFileDialog.getOpenFileName(caption=i18n("Which image should be the basis the new page?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0], self.projecturl) newPage = Application.openDocument(os.path.join(self.projecturl, templateUrl)) newPage.waitForDone() newPage.setFileName(absoluteUrl) newPage.setName(pageName.replace("_", " ")) newPage.save() newPage.waitForDone() # Get out the extra data for the standard item. newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(256, 256)))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(pageName.replace("_", " ")) newPageItem.setData("", role = CPE.DESCRIPTION) newPageItem.setData(url, role = CPE.URL) newPageItem.setData("", role = CPE.KEYWORDS) newPageItem.setData("", role = CPE.LASTEDIT) newPageItem.setData("", role = CPE.EDITOR) newPageItem.setToolTip(url) # close page document. while os.path.exists(absoluteUrl) is False: qApp.processEvents() self.pagesWatcher.addPath(absoluteUrl) newPage.close() # add item to page. self.pagesModel.appendRow(newPageItem) """ Write to the json configuration file. This also checks the current state of the pages list. """ def slot_write_config(self): # Don't load when the pages are still being loaded, otherwise we'll be overwriting our own pages list. if (self.loadingPages is False): print("CPMT: writing comic configuration...") # Generate a pages list from the pagesmodel. pagesList = [] for i in range(self.pagesModel.rowCount()): index = self.pagesModel.index(i, 0) url = str(self.pagesModel.data(index, role=CPE.URL)) if url not in pagesList: pagesList.append(url) self.setupDictionary["pages"] = pagesList # Save to our json file. configFile = open(self.path_to_config, "w", newline="", encoding="utf-16") json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) configFile.close() print("CPMT: done") """ Open a page in the pagesmodel in Krita. """ def slot_open_page(self, index): if index.column() is 0: # Get the absolute url from the relative one in the pages model. absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=CPE.URL))) # Make sure the page exists. if os.path.exists(absoluteUrl): page = Application.openDocument(absoluteUrl) # Set the title to the filename if it was empty. It looks a bit neater. if page.name().isspace or len(page.name()) < 1: page.setName(str(self.pagesModel.data(index, role=Qt.DisplayRole)).replace("_", " ")) # Add views for the document so the user can use it. Application.activeWindow().addView(page) Application.setActiveDocument(page) else: print("CPMT: The page cannot be opened because the file doesn't exist:", absoluteUrl) """ Call up the metadata editor dialog. Only when the dialog is "Accepted" will the metadata be saved. """ def slot_edit_meta_data(self): dialog = comics_metadata_dialog.comic_meta_data_editor() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ An attempt at making the description editable from the comic pages list. It is currently not working because ZipFile has no overwrite mechanism, and I don't have the energy to write one yet. """ def slot_write_description(self, index): for row in range(self.pagesModel.rowCount()): index = self.pagesModel.index(row, 1) indexUrl = self.pagesModel.index(row, 0) absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(indexUrl, role=CPE.URL))) page = zipfile.ZipFile(absoluteUrl, "a") xmlDoc = ET.ElementTree() ET.register_namespace("", "http://www.calligra.org/DTD/document-info") location = os.path.join(self.projecturl, "documentinfo.xml") xmlDoc.parse(location) xmlroot = ET.fromstring(page.read("documentinfo.xml")) calligra = "{http://www.calligra.org/DTD/document-info}" aboutelem = xmlroot.find(calligra + 'about') if ET.iselement(aboutelem.find(calligra + 'subject')): desc = aboutelem.find(calligra + 'subject') desc.text = self.pagesModel.data(index, role=Qt.EditRole) xmlstring = ET.tostring(xmlroot, encoding='unicode', method='xml', short_empty_elements=False) page.writestr(zinfo_or_arcname="documentinfo.xml", data=xmlstring) for document in Application.documents(): if str(document.fileName()) == str(absoluteUrl): document.setDocumentInfo(xmlstring) page.close() """ Calls up the export settings dialog. Only when accepted will the configuration be written. """ def slot_edit_export_settings(self): dialog = comics_export_dialog.comic_export_setting_dialog() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ Export the comic. Won't work without export settings set. """ def slot_export(self): #ensure there is a unique identifier if "uuid" not in self.setupDictionary.keys(): uuid = str() if "acbfID" in self.setupDictionary.keys(): uuid = str(self.setupDictionary["acbfID"]) else: uuid = QUuid.createUuid().toString() self.setupDictionary["uuid"] = uuid exporter = comics_exporter.comicsExporter() exporter.set_config(self.setupDictionary, self.projecturl) exportSuccess = exporter.export() if exportSuccess: print("CPMT: Export success! The files have been written to the export folder!") QMessageBox.information(self, i18n("Export success"), i18n("The files have been written to the export folder."), QMessageBox.Ok) """ Calls up the comics project setup wizard so users can create a new json file with the basic information. """ def slot_new_project(self): setup = comics_project_setup_wizard.ComicsProjectSetupWizard() setup.showDialog() self.path_to_config = os.path.join(setup.projectDirectory, "comicConfig.json") if os.path.exists(self.path_to_config) is True: configFile = open(self.path_to_config, "r", newline="", encoding="utf-16") self.setupDictionary = json.load(configFile) self.projecturl = os.path.dirname(str(self.path_to_config)) configFile.close() self.load_config() """ This is triggered by any document save. It checks if the given url in in the pages list, and if so, updates the appropriate page thumbnail. This helps with the management of the pages, because the user will be able to see the thumbnails as a todo for the whole comic, giving a good overview over whether they still need to ink, color or the like for a given page, and it thus also rewards the user whenever they save. """ def slot_check_for_page_update(self, url): if "pages" in self.setupDictionary.keys(): relUrl = os.path.relpath(url, self.projecturl) if relUrl in self.setupDictionary["pages"]: index = self.pagesModel.index(self.setupDictionary["pages"].index(relUrl), 0) if index.isValid(): if os.path.exists(url) is False: # we cannot check from here whether the file in question has been renamed or deleted. self.pagesModel.removeRow(index.row()) return else: # Krita will trigger the filesystemwatcher when doing backupfiles, # so ensure the file is still watched if it exists. self.pagesWatcher.addPath(url) pageItem = self.pagesModel.itemFromIndex(index) page = zipfile.ZipFile(url, "r") dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) thumbnail = QImage.fromData(page.read("preview.png")) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setText(dataList[0]) pageItem.setData(dataList[1], role = CPE.DESCRIPTION) pageItem.setData(relUrl, role = CPE.URL) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) self.pagesModel.setItem(index.row(), index.column(), pageItem) """ Resize all the pages in the pages list. It will show a dialog with the options for resizing. Then, it will try to pop up a progress dialog while resizing. The progress dialog shows the remaining time and pages. """ def slot_batch_resize(self): dialog = QDialog() dialog.setWindowTitle(i18n("Resize all Pages")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) sizesBox = comics_export_dialog.comic_export_resize_widget("Scale", batch=True, fileType=False) exporterSizes = comics_exporter.sizesCalculator() dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(sizesBox) dialog.layout().addWidget(buttons) if dialog.exec_() == QDialog.Accepted: progress = QProgressDialog(i18n("Resizing pages..."), str(), 0, len(self.setupDictionary["pages"])) progress.setWindowTitle(i18n("Resizing Pages")) progress.setCancelButton(None) timer = QElapsedTimer() timer.start() config = {} config = sizesBox.get_config(config) for p in range(len(self.setupDictionary["pages"])): absoluteUrl = os.path.join(self.projecturl, self.setupDictionary["pages"][p]) progress.setValue(p) timePassed = timer.elapsed() if (p > 0): timeEstimated = (len(self.setupDictionary["pages"]) - p) * (timePassed / p) passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(self.setupDictionary["pages"]), passedString=passedString, estimated=estimatedString)) qApp.processEvents() if os.path.exists(absoluteUrl): doc = Application.openDocument(absoluteUrl) listScales = exporterSizes.get_scale_from_resize_config(config["Scale"], [doc.width(), doc.height(), doc.resolution(), doc.resolution()]) doc.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") doc.waitForDone() doc.save() doc.waitForDone() doc.close() def slot_show_page_viewer(self): index = int(self.comicPageList.currentIndex().row()) self.page_viewer_dialog.load_comic(self.path_to_config) self.page_viewer_dialog.go_to_page_index(index) self.page_viewer_dialog.show() """ Function to copy the current project location into the clipboard. This is useful for users because they'll be able to use that url to quickly move to the project location in outside applications. """ def slot_copy_project_url(self): if self.projecturl is not None: clipboard = qApp.clipboard() clipboard.setText(str(self.projecturl)) """ Scrape text files with the textlayer keys for text, and put those in a POT file. This makes it possible to handle translations. """ def slot_scrape_translations(self): translationFolder = self.setupDictionary.get("translationLocation", "translations") fullTranslationPath = os.path.join(self.projecturl, translationFolder) os.makedirs(fullTranslationPath, exist_ok=True) textLayersToSearch = self.setupDictionary.get("textLayerNames", ["text"]) scraper = comics_project_translation_scraper.translation_scraper(self.projecturl, translationFolder, textLayersToSearch, self.setupDictionary["projectName"]) # Run text scraper. language = self.setupDictionary.get("language", "en") metadata = {} metadata["title"] = self.setupDictionary.get("title", "") metadata["summary"] = self.setupDictionary.get("summary", "") metadata["keywords"] = ", ".join(self.setupDictionary.get("otherKeywords", [""])) metadata["transnotes"] = self.setupDictionary.get("translatorHeader", "Translator's Notes") scraper.start(self.setupDictionary["pages"], language, metadata) QMessageBox.information(self, i18n("Scraping success"), str(i18n("POT file has been written to: {file}")).format(file=fullTranslationPath), QMessageBox.Ok) """ This is required by the dockwidget class, otherwise unused. """ def canvasChanged(self, canvas): pass
class Data_Dialog(QDialog): def __init__(self, fname=None, data=None, comment='#', skiprows=0, delimiter=' ', expressions={}, autoupdate=False, parent=None, matplotlib=False, plotIndex=None, colors=None): QDialog.__init__(self, parent=parent) loadUi('UI_Forms/Data_Dialog.ui', self) self.colcycler = cycle(['r', 'g', 'b', 'c', 'm', 'y', 'w']) self.plotWidget = PlotWidget(parent=self, matplotlib=matplotlib) self.plotTab = self.tabWidget.addTab(self.plotWidget, 'Plots') self.tabWidget.setCurrentIndex(0) self.show() self.fileWatcher = QFileSystemWatcher() self.fileWatcher.fileChanged.connect(self.fileUpdated) self.cwd = None self.plotNum = 0 self.xlabel = [] self.ylabel = [] self.oldPlotIndex = {} self.oldColors = {} self.dataAltered = False self.expressions = expressions if data is not None: self.data = data self.autoUpdateCheckBox.setEnabled(False) elif fname is not None: self.data = self.readData(fname, comment=comment, skiprows=skiprows, delimiter=delimiter) else: self.data = None self.autoUpdateCheckBox.setEnabled(False) self.saveDataPushButton.setEnabled(False) self.addRowPushButton.setEnabled(False) self.removeRowsPushButton.setEnabled(False) self.removeColumnPushButton.setEnabled(False) if self.data is not None: self.setMeta2Table() self.setData2Table() if plotIndex is None: self.addPlots(color=None) else: self.addMultiPlots(plotIndex=plotIndex, colors=colors) self.init_signals() self.okPushButton.setAutoDefault(False) self.make_default() self.setWindowTitle('Data Dialog') self.acceptData = True #self.setWindowSize((600,400)) # if self.parentWidget() is not None: # self.addPlotPushButton.setEnabled(False) # self.removePlotPushButton.setEnabled(False) def make_default(self): self.okPushButton.setAutoDefault(False) self.closePushButton.setAutoDefault(False) self.openDataFilePushButton.setAutoDefault(False) self.saveDataPushButton.setAutoDefault(False) self.okPushButton.setDefault(False) self.closePushButton.setDefault(False) self.openDataFilePushButton.setDefault(False) self.saveDataPushButton.setDefault(False) def init_signals(self): self.closePushButton.clicked.connect(self.closeWidget) self.okPushButton.clicked.connect(self.acceptWidget) self.openDataFilePushButton.clicked.connect(self.openFile) self.autoUpdateCheckBox.stateChanged.connect(self.autoUpdate_ON_OFF) self.saveDataPushButton.clicked.connect(self.saveData) self.addPlotPushButton.clicked.connect( lambda x: self.addPlots(plotIndex=None)) self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData) self.removePlotPushButton.clicked.connect(self.removePlots) self.addMetaDataPushButton.clicked.connect(self.addMetaData) self.metaDataTableWidget.itemChanged.connect(self.metaDataChanged) self.metaDataTableWidget.itemClicked.connect(self.metaDataClicked) self.metaDataTableWidget.itemSelectionChanged.connect( self.metaDataSelectionChanged) self.removeMetaDataPushButton.clicked.connect(self.removeMetaData) self.dataTableWidget.itemChanged.connect(self.dataChanged) self.editColumnPushButton.clicked.connect(self.editDataColumn) self.addColumnPushButton.clicked.connect( lambda x: self.addDataColumn(colName='Col_X')) self.removeColumnPushButton.clicked.connect(self.removeDataColumn) self.removeRowsPushButton.clicked.connect(self.removeDataRows) self.dataTableWidget.setSelection self.dataTableWidget.horizontalHeader().sortIndicatorChanged.connect( self.dataSorted) self.addRowPushButton.clicked.connect(self.addDataRow) def closeWidget(self): self.acceptData = False self.reject() def acceptWidget(self): self.acceptData = True self.accept() def addMetaData(self): """ Opens a MetaData Dialog and by accepting the dialog inputs the data to the MetaDataTable """ self.metaDialog = MetaData_Dialog() if self.metaDialog.exec_(): name, value = self.metaDialog.parNameLineEdit.text( ), self.metaDialog.parValueLineEdit.text() if name not in self.data['meta'].keys(): row = self.metaDataTableWidget.rowCount() self.metaDataTableWidget.insertRow(row) self.metaDataTableWidget.setItem(row, 0, QTableWidgetItem(name)) self.metaDataTableWidget.setItem(row, 1, QTableWidgetItem(value)) try: self.data['meta'][name] = eval(value) except: self.data['meta'][name] = value else: QMessageBox.warning( self, "Parameter Exists", "The parameter %s already exists in meta data. Please provide a different parameter name" % name, QMessageBox.Ok) self.addMetaData() def removeMetaData(self): """ Removes the selected Metadata from the table """ self.metaDataTableWidget.itemSelectionChanged.disconnect() rows = list( set([ item.row() for item in self.metaDataTableWidget.selectedItems() ])) for row in rows: key = self.metaDataTableWidget.item(row, 0).text() if key != 'col_names': del self.data['meta'][key] self.metaDataTableWidget.removeRow(row) else: QMessageBox.warning(self, 'Restricted Parameter', 'You cannot delete the parameter %s' % key, QMessageBox.Ok) self.metaDataTableWidget.itemSelectionChanged.connect( self.metaDataSelectionChanged) def metaDataChanged(self, item): """ Updates the value metadata as per the changes in the metaDataTableWidget """ row = item.row() col = item.column() key = self.metaDataTableWidget.item(row, 0).text() if col != 0: try: self.data['meta'][key] = eval(item.text()) except: self.data['meta'][key] = item.text() if self.metaDataTableWidget.item( row, 0).text() == 'col_names' and len( self.data['meta'][key]) != len( self.data['data'].columns): QMessageBox.warning( self, 'Restricted Parameter', 'Please provide same length of col_names as the number of the column of the data' ) self.data['meta'][key] = eval(self.oldMetaText) item.setText(self.oldMetaText) elif self.metaDataTableWidget.item( row, 0).text() == 'col_names' and len( self.data['meta'][key]) == len( self.data['data'].columns): self.data['data'].columns = self.data['meta'][key] self.dataTableWidget.setHorizontalHeaderLabels( self.data['meta'][key]) self.dataAltered = True self.resetPlotSetup() self.dataAltered = False else: if self.oldMetaText == 'col_names': QMessageBox.warning( self, 'Restricted Parameter', 'col_names is a restricted parameter the name of which cannot be changed', QMessageBox.Ok) item.setText(self.oldMetaText) elif item.text() not in self.data['meta'].keys(): self.data['meta'][key] = self.data['meta'][self.oldMetaText] del self.data['meta'][self.oldMetaText] else: self.metaDataTableWidget.itemChanged.disconnect() QMessageBox.warning( self, "Parameter Exists", "The parameter %s already exists in meta data. Please provide a different parameter name" % item.text(), QMessageBox.Ok) item.setText(self.oldMetaText) self.metaDataTableWidget.itemChanged.connect( self.metaDataChanged) self.oldMetaText = item.text() def metaDataClicked(self, item): self.oldMetaText = item.text() def metaDataSelectionChanged(self): self.oldMetaText = self.metaDataTableWidget.selectedItems()[0].text() def dataChanged(self, item): row, col = item.row(), item.column() key = self.dataTableWidget.horizontalHeaderItem(col).text() self.data['data'][key][row] = eval(item.text()) self.dataAltered = True self.resetPlotSetup() self.dataAltered = False def dataSorted(self): """ Updates the data after sorting the DataTableWidget """ self.getDataFromTable() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False def addDataRow(self): try: self.dataTableWidget.itemChanged.disconnect() except: pass row = self.dataTableWidget.currentRow() self.dataTableWidget.insertRow(row + 1) for col in range(self.dataTableWidget.columnCount()): self.dataTableWidget.setItem( row + 1, col, QCustomTableWidgetItem( float(self.dataTableWidget.item(row, col).text()))) self.getDataFromTable() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False self.dataTableWidget.itemChanged.connect(self.dataChanged) def editDataColumn(self): if self.data is not None: items = self.dataTableWidget.selectedItems() selCols = list([item.column() for item in items]) if len(selCols) == 1: colName = self.dataTableWidget.horizontalHeaderItem( selCols[0]).text() self.addDataColumn(colName=colName, expr=self.expressions[colName], new=False) else: QMessageBox.warning( self, 'Column Selection Error', 'Please select only elements of a single column.', QMessageBox.Ok) else: QMessageBox.warning(self, 'Data error', 'There is no data', QMessageBox.Ok) def addDataColumn(self, colName='Col_X', expr=None, new=True): if self.data is not None: row, col = self.data['data'].shape self.insertColDialog = InsertCol_Dialog(colName=colName, minCounter=1, maxCounter=row, expr=expr) if self.insertColDialog.exec_(): imin = eval(self.insertColDialog.minCounterLineEdit.text()) imax = eval(self.insertColDialog.maxCounterLineEdit.text()) i = arange(imin, imax + 1) colname = self.insertColDialog.colNameLineEdit.text() data = copy.copy(self.data) if new: if colname not in self.data['data'].columns: try: self.data['data'][colname] = eval(expr) except: try: expr = self.insertColDialog.colExprTextEdit.toPlainText( ) cexpr = expr.replace('col', "self.data['data']") self.data['data'][colname] = eval(cexpr) self.data['meta']['col_names'].append(colname) except: QMessageBox.warning( self, 'Column Error', 'Please check the expression.\n The expression should be in this format:\n col[column_name]*5', QMessageBox.Ok) self.addDataColumn(colName='Col_X', expr=expr) self.expressions[colname] = expr self.setData2Table() self.setMeta2Table() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False else: QMessageBox.warning( self, 'Column Name Error', 'Please choose different column name than the exisiting ones', QMessageBox.Ok) self.addDataColumn(colName='Col_X', expr=expr) else: try: self.data['data'][colname] = eval(expr) except: try: expr = self.insertColDialog.colExprTextEdit.toPlainText( ) cexpr = expr.replace('col', "self.data['data\']") self.data['data'][colname] = eval(cexpr) except: QMessageBox.warning( self, 'Column Error', 'Please check the expression.\n The expression should be in this format:\n col[column_name]*5', QMessageBox.Ok) self.addDataColumn(colName='Col_X', expr=expr) self.expressions[colname] = expr self.setData2Table() self.setMeta2Table() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False else: self.data = {} self.insertColDialog = InsertCol_Dialog(colName=colName, minCounter=1, maxCounter=100, expr=expr) if self.insertColDialog.exec_(): imin = eval(self.insertColDialog.minCounterLineEdit.text()) imax = eval(self.insertColDialog.maxCounterLineEdit.text()) i = arange(imin, imax + 1) colname = self.insertColDialog.colNameLineEdit.text() expr = self.insertColDialog.colExprTextEdit.toPlainText() expr = expr.replace('col.', "self.data['data']") try: self.data['data'] = pd.DataFrame(eval(expr), columns=[colname]) self.data['meta'] = {} self.data['meta']['col_names'] = [colname] self.setData2Table() self.setMeta2Table() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False self.saveDataPushButton.setEnabled(True) self.addRowPushButton.setEnabled(True) self.removeRowsPushButton.setEnabled(True) self.removeColumnPushButton.setEnabled(True) self.expressions[colname] = expr except: QMessageBox.warning( self, 'Column Error', 'Please check the expression.\n The expression should be in this format:\n col.column_name*5', QMessageBox.Ok) self.data = None self.addDataColumn(colName='Col_X', expr=expr) def removeDataColumn(self): """ Removes selected columns from dataTableWidget """ colIndexes = [ index.column() for index in self.dataTableWidget.selectionModel().selectedColumns() ] colIndexes.sort(reverse=True) if self.dataTableWidget.columnCount() - len( colIndexes) >= 2 or self.plotSetupTableWidget.rowCount() == 0: for index in colIndexes: colname = self.data['meta']['col_names'][index] self.data['meta']['col_names'].pop(index) del self.expressions[colname] self.dataTableWidget.removeColumn(index) if self.dataTableWidget.columnCount() != 0: self.getDataFromTable() self.setMeta2Table() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False else: self.data['data'] = None self.dataTableWidget.clear() #self.metaDataTableWidget.clear() self.autoUpdateCheckBox.setEnabled(False) self.saveDataPushButton.setEnabled(False) self.addRowPushButton.setEnabled(False) self.removeRowsPushButton.setEnabled(False) self.removeColumnPushButton.setEnabled(False) else: QMessageBox.warning( self, 'Remove Error', 'Cannot remove these many columns because Data Dialog needs to have atleast two columns', QMessageBox.Ok) def removeDataRows(self): rowIndexes = [ index.row() for index in self.dataTableWidget.selectionModel().selectedRows() ] rowIndexes.sort(reverse=True) if len(rowIndexes) > 0: ans = QMessageBox.question( self, 'Confirmation', 'Are you sure of removing the selected rows?', QMessageBox.Yes, QMessageBox.No) if ans == QMessageBox.Yes: for i in rowIndexes: self.dataTableWidget.removeRow(i) self.getDataFromTable() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False def setMeta2Table(self): """ Populates the metaDataTable widget with metadata available from the data """ try: self.metaDataTableWidget.itemChanged.disconnect() self.metaDataTableWidget.itemSelectionChanged.disconnect() except: pass self.metaDataTableWidget.clear() self.metaDataTableWidget.setColumnCount(2) self.metaDataTableWidget.setRowCount(len(self.data['meta'].keys())) for num, key in enumerate(self.data['meta'].keys()): self.metaDataTableWidget.setItem(num, 0, QTableWidgetItem(key)) self.metaDataTableWidget.setItem( num, 1, QTableWidgetItem(str(self.data['meta'][key]))) if 'col_names' not in self.data['meta'].keys(): self.data['meta']['col_names'] = self.data['data'].columns.tolist() self.metaDataTableWidget.insertRow( self.metaDataTableWidget.rowCount()) self.metaDataTableWidget.setItem(num + 1, 0, QTableWidgetItem('col_names')) self.metaDataTableWidget.setItem( num + 1, 1, QTableWidgetItem(str(self.data['meta']['col_names']))) self.metaDataTableWidget.setHorizontalHeaderLabels( ['Parameter', 'Value']) self.metaDataTableWidget.itemChanged.connect(self.metaDataChanged) self.metaDataTableWidget.itemSelectionChanged.connect( self.metaDataSelectionChanged) def getMetaFromTable(self): self.data['meta'] = {} for i in range(self.metaDataTableWidget.rowCount()): try: self.data['meta'][self.metaDataTableWidget.item( i, 0).text()] = eval( self.metaDataTableWidget.item(i, 1).text()) except: self.data['meta'][self.metaDataTableWidget.item( i, 0).text()] = self.metaDataTableWidget.item(i, 1).text() def setData2Table(self): """ Populates the dataTableWidget with data available from data """ try: self.dataTableWidget.itemChanged.disconnect() except: pass self.dataTableWidget.clear() self.dataTableWidget.setColumnCount(len(self.data['data'].columns)) self.dataTableWidget.setRowCount(len(self.data['data'].index)) for j, colname in enumerate(self.data['data'].columns): if colname not in self.expressions.keys(): self.expressions[colname] = "col['%s']" % colname for i in range(len(self.data['data'].index)): #self.dataTableWidget.setItem(i,j,QTableWidgetItem(str(self.data['data'][colname][i]))) self.dataTableWidget.setItem( i, j, QCustomTableWidgetItem(self.data['data'][colname][i])) self.dataTableWidget.setHorizontalHeaderLabels( self.data['data'].columns.values.tolist()) self.dataTableWidget.itemChanged.connect(self.dataChanged) def getDataFromTable(self): self.data['data'] = pd.DataFrame() for col in range(self.dataTableWidget.columnCount()): label = self.dataTableWidget.horizontalHeaderItem(col).text() self.data['data'][label] = array([ float(self.dataTableWidget.item(i, col).text()) for i in range(self.dataTableWidget.rowCount()) ]) def readData(self, fname, skiprows=0, comment='#', delimiter=' '): """ Read data from a file and put it in dictionary structure with keys 'meta' and 'data' and the data would look like the following data={'meta':meta_dictionary,'data'=pandas_dataframe} """ if os.path.exists(os.path.abspath(fname)): self.data = {} self.fname = fname self.dataFileLineEdit.setText(self.fname) self.cwd = os.path.dirname(self.fname) fh = open(os.path.abspath(self.fname), 'r') lines = fh.readlines() fh.close() self.data['meta'] = {} for line in lines[skiprows:]: if line[0] == comment: try: key, value = line[1:].strip().split('=') try: self.data['meta'][key] = eval( value ) # When the value is either valid number, lists, arrays, dictionaries except: self.data['meta'][ key] = value # When the value is just a string except: pass else: if '\t' in line: delimiter = '\t' elif ',' in line: delimiter = ',' elif ' ' in line: delimiter = ' ' break if 'col_names' in self.data['meta'].keys(): self.data['data'] = pd.read_csv( self.fname, comment=comment, names=self.data['meta']['col_names'], header=None, sep=delimiter) if not all(self.data['data'].isnull().values): self.data['data'] = pd.DataFrame( loadtxt(self.fname, skiprows=skiprows), columns=self.data['meta']['col_names']) else: self.data['data'] = pd.read_csv(self.fname, comment=comment, header=None, sep=delimiter) if not all(self.data['data'].isnull()): self.data['data'] = pd.DataFrame( loadtxt(self.fname, skiprows=skiprows)) self.data['data'].columns = [ 'Col_%d' % i for i in self.data['data'].columns.values.tolist() ] self.data['meta']['col_names'] = self.data[ 'data'].columns.values.tolist() self.autoUpdate_ON_OFF() self.autoUpdateCheckBox.setEnabled(True) self.saveDataPushButton.setEnabled(True) self.addRowPushButton.setEnabled(True) self.removeRowsPushButton.setEnabled(True) self.removeColumnPushButton.setEnabled(True) return self.data else: QMessageBox.warning(self, 'File Error', 'The file doesnot exists!') return None def fileUpdated(self, fname): QTest.qWait(1000) self.readData(fname=fname) if self.data is not None: self.setMeta2Table() self.setData2Table() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False def autoUpdate_ON_OFF(self): files = self.fileWatcher.files() if len(files) != 0: self.fileWatcher.removePaths(files) if self.autoUpdateCheckBox.isChecked(): self.fileWatcher.addPath(self.fname) def saveData(self): """ Save data to a file """ fname = QFileDialog.getSaveFileName(self, 'Save file as', self.cwd, filter='*.*')[0] if fname != '': ext = os.path.splitext(fname)[1] if ext == '': ext = '.txt' fname = fname + ext header = 'File saved on %s\n' % time.asctime() for key in self.data['meta'].keys(): header = header + '%s=%s\n' % (key, str( self.data['meta'][key])) if 'col_names' not in self.data['meta'].keys(): header = header + 'col_names=%s\n' % str( self.data['data'].columns.tolist()) savetxt(fname, self.data['data'].values, header=header, comments='#') def openFile(self): """ Opens a openFileDialog to open a data file """ if self.cwd is not None: fname = QFileDialog.getOpenFileName(self, 'Select a data file to open', directory=self.cwd, filter='*.*')[0] else: fname = QFileDialog.getOpenFileName(self, 'Select a data file to open', directory='', filter='*.*')[0] if fname != '': self.data = self.readData(fname=fname) if self.data is not None: self.setMeta2Table() self.setData2Table() self.dataAltered = True self.resetPlotSetup() self.dataAltered = False def resetPlotSetup(self): try: self.plotSetupTableWidget.cellChanged.disconnect() except: pass columns = self.data['data'].columns.tolist() self.xlabel = [] self.ylabel = [] for row in range(self.plotSetupTableWidget.rowCount()): for i in range(1, 3): self.plotSetupTableWidget.cellWidget( row, i).currentIndexChanged.disconnect() self.plotSetupTableWidget.cellWidget(row, i).clear() self.plotSetupTableWidget.cellWidget(row, i).addItems(columns) self.plotSetupTableWidget.cellWidget(row, i).setCurrentIndex(i - 1) self.plotSetupTableWidget.cellWidget( row, i).currentIndexChanged.connect(self.updateCellData) self.xlabel.append( '[%s]' % self.plotSetupTableWidget.cellWidget(row, 1).currentText()) self.ylabel.append( '[%s]' % self.plotSetupTableWidget.cellWidget(row, 2).currentText()) self.plotSetupTableWidget.cellWidget( row, 3).currentIndexChanged.disconnect() self.plotSetupTableWidget.cellWidget(row, 3).clear() self.plotSetupTableWidget.cellWidget(row, 3).addItems(['None'] + columns) self.plotSetupTableWidget.cellWidget(row, 3).setCurrentIndex(0) self.plotSetupTableWidget.cellWidget( row, 3).currentIndexChanged.connect(self.updateCellData) self.plotSetupTableWidget.setCurrentCell(row, 3) color = self.plotSetupTableWidget.cellWidget(row, 4).color() self.plotSetupTableWidget.setCellWidget( row, 4, pg.ColorButton(color=color)) self.plotSetupTableWidget.cellWidget( row, 4).sigColorChanging.connect(self.updateCellData) self.plotSetupTableWidget.cellWidget( row, 4).sigColorChanged.connect(self.updateCellData) self.updatePlotData(row, i) self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData) def addMultiPlots(self, plotIndex=None, colors=None): for key in plotIndex.keys(): pi = plotIndex[key] if colors is None: color = next(self.colcycler ) #array([random.randint(200, high=255),0,0]) print(color) else: color = colors[key] self.addPlots(plotIndex=pi, color=color) def addPlots(self, plotIndex=None, color=None): #self.plotSetupTableWidget.clear() # if self.parentWidget() is None or self.plotSetupTableWidget.rowCount()==0: try: self.plotSetupTableWidget.cellChanged.disconnect() except: pass columns = self.data['data'].columns.tolist() if len(columns) >= 2: self.plotSetupTableWidget.insertRow( self.plotSetupTableWidget.rowCount()) row = self.plotSetupTableWidget.rowCount() - 1 self.plotSetupTableWidget.setItem( row, 0, QTableWidgetItem('Data_%d' % self.plotNum)) for i in range(1, 3): self.plotSetupTableWidget.setCellWidget(row, i, QComboBox()) self.plotSetupTableWidget.cellWidget(row, i).addItems(columns) if plotIndex is not None: self.plotSetupTableWidget.cellWidget( row, i).setCurrentIndex(plotIndex[i - 1]) else: self.plotSetupTableWidget.cellWidget( row, i).setCurrentIndex(i - 1) self.plotSetupTableWidget.cellWidget( row, i).currentIndexChanged.connect(self.updateCellData) self.xlabel.append( '[%s]' % self.plotSetupTableWidget.cellWidget(row, 1).currentText()) self.ylabel.append( '[%s]' % self.plotSetupTableWidget.cellWidget(row, 2).currentText()) self.plotSetupTableWidget.setCellWidget(row, 3, QComboBox()) self.plotSetupTableWidget.cellWidget(row, 3).addItems(['None'] + columns) if color is None: color = next(self.colcycler ) #array([random.randint(200, high=255),0,0]) self.plotSetupTableWidget.setCellWidget( row, 4, pg.ColorButton(color=color)) self.plotSetupTableWidget.cellWidget( row, 4).sigColorChanging.connect(self.updateCellData) self.plotSetupTableWidget.cellWidget( row, 4).sigColorChanged.connect(self.updateCellData) if plotIndex is not None: self.plotSetupTableWidget.cellWidget(row, 3).setCurrentIndex( plotIndex[-1]) else: # try: # self.plotSetupTableWidget.cellWidget(row,3).setCurrentIndex(2) # except: # self.plotSetupTableWidget.cellWidget(row, 3).setCurrentIndex(0) self.plotSetupTableWidget.cellWidget( row, 3).currentIndexChanged.connect(self.updateCellData) self.plotSetupTableWidget.setCurrentCell(row, 3) self.updatePlotData(row, 3) self.plotNum += 1 else: QMessageBox.warning( self, 'Data file error', 'The data file do not have two or more columns to be plotted.', QMessageBox.Ok) self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData) # else: # QMessageBox.warning(self,'Warning','As the Data Dialog is used within another widget you cannot add more plots',QMessageBox.Ok) def removePlots(self): """ Removes data for PlotSetup """ try: self.plotSetupTableWidget.cellChanged.disconnect() except: pass rowIndexes = self.plotSetupTableWidget.selectionModel().selectedRows() selectedRows = [index.row() for index in rowIndexes] selectedRows.sort(reverse=True) if self.parentWidget() is None: for row in selectedRows: name = self.plotSetupTableWidget.item(row, 0).text() self.plotWidget.remove_data([name]) self.plotSetupTableWidget.removeRow(row) else: if self.plotSetupTableWidget.rowCount() - len(rowIndexes) >= 1: for row in selectedRows: name = self.plotSetupTableWidget.item(row, 0).text() self.plotWidget.remove_data([name]) self.plotSetupTableWidget.removeRow(row) else: QMessageBox.warning( self, 'Warning', 'Cannot remove single plots from Data Dialog because the Data Dialog is used within another widget', QMessageBox.Ok) self.updatePlot() self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData) def updatePlotData(self, row, col): #row=self.plotSetupTableWidget.currentRow() name = self.plotSetupTableWidget.item(row, 0).text() if self.dataAltered: for i in range(1, 4): try: self.plotSetupTableWidget.cellWidget( row, i).setCurrentIndex(self.oldPlotIndex[name][i - 1]) except: pass xcol, ycol, yerrcol = [ self.plotSetupTableWidget.cellWidget(row, i).currentText() for i in range(1, 4) ] #ycol=self.plotSetupTableWidget.cellWidget(row,2).currentText() #yerrcol=self.plotSetupTableWidget.cellWidget(row,3).currentText() if yerrcol != 'None': if ycol == 'fit': self.plotWidget.add_data( self.data['data'][xcol].values, self.data['data'][ycol].values, yerr=self.data['data'][yerrcol].values, name=name, fit=True, color=self.plotSetupTableWidget.cellWidget(row, 4).color()) else: self.plotWidget.add_data( self.data['data'][xcol].values, self.data['data'][ycol].values, yerr=self.data['data'][yerrcol].values, name=name, fit=False, color=self.plotSetupTableWidget.cellWidget(row, 4).color()) else: if ycol == 'fit': self.plotWidget.add_data( self.data['data'][xcol].values, self.data['data'][ycol].values, name=name, fit=True, color=self.plotSetupTableWidget.cellWidget(row, 4).color()) else: self.plotWidget.add_data( self.data['data'][xcol].values, self.data['data'][ycol].values, name=name, fit=False, color=self.plotSetupTableWidget.cellWidget(row, 4).color()) self.xlabel[row] = '[%s]' % self.plotSetupTableWidget.cellWidget( row, 1).currentText() self.ylabel[row] = '[%s]' % self.plotSetupTableWidget.cellWidget( row, 2).currentText() self.updatePlot() self.oldPlotIndex[name] = [ self.plotSetupTableWidget.cellWidget(row, i).currentIndex() for i in range(1, 4) ] def updateCellData(self, index): row = self.plotSetupTableWidget.indexAt(self.sender().pos()).row() self.updatePlotData(row, index) def updatePlot(self): self.make_default() names = [ self.plotSetupTableWidget.item(i, 0).text() for i in range(self.plotSetupTableWidget.rowCount()) ] #self.plotColIndex=[self.plotSetupTableWidget.cellWidget(0,i).currentIndex() for i in range(1,4)] self.plotColIndex = {} self.externalData = {} self.plotColors = {} for i in range(self.plotSetupTableWidget.rowCount()): key = self.plotSetupTableWidget.cellWidget(i, 2).currentText() self.plotColIndex[key] = [ self.plotSetupTableWidget.cellWidget(i, j).currentIndex() for j in range(1, 4) ] self.plotColors[key] = self.plotSetupTableWidget.cellWidget( i, 4).color() self.externalData[key] = copy.copy(self.data['meta']) self.externalData[key]['x'] = copy.copy( self.data['data'][self.plotSetupTableWidget.cellWidget( i, 1).currentText()].values) self.externalData[key]['y'] = copy.copy( self.data['data'][self.plotSetupTableWidget.cellWidget( i, 2).currentText()].values) if self.plotSetupTableWidget.cellWidget(i, 3).currentText() == 'None': self.externalData[key]['yerr'] = ones_like( self.externalData[key]['x']) else: self.externalData[key]['yerr'] = copy.copy( self.data['data'][self.plotSetupTableWidget.cellWidget( i, 3).currentText()].values) self.externalData[key][ 'color'] = self.plotSetupTableWidget.cellWidget(i, 4).color() self.plotWidget.Plot(names) self.plotWidget.setXLabel(' '.join(self.xlabel)) self.plotWidget.setYLabel(' '.join(self.ylabel))
class ProjectBrowserModel(BrowserModel): """ Class implementing the project browser model. @signal vcsStateChanged(str) emitted after the VCS state has changed """ vcsStateChanged = pyqtSignal(str) def __init__(self, parent): """ Constructor @param parent reference to parent object (Project.Project) """ super(ProjectBrowserModel, self).__init__(parent, nopopulate=True) rootData = self.tr("Name") self.rootItem = BrowserItem(None, rootData) self.rootItem.itemData.append(self.tr("VCS Status")) self.progDir = None self.project = parent self.watchedItems = {} self.watcher = QFileSystemWatcher(self) self.watcher.directoryChanged.connect(self.directoryChanged) self.inRefresh = False self.projectBrowserTypes = { "SOURCES": ProjectBrowserSourceType, "FORMS": ProjectBrowserFormType, "RESOURCES": ProjectBrowserResourceType, "INTERFACES": ProjectBrowserInterfaceType, "TRANSLATIONS": ProjectBrowserTranslationType, "OTHERS": ProjectBrowserOthersType, } self.colorNames = { "A": "VcsAdded", "M": "VcsModified", "O": "VcsRemoved", "R": "VcsReplaced", "U": "VcsUpdate", "Z": "VcsConflict", } self.itemBackgroundColors = { " ": QColor(), "A": Preferences.getProjectBrowserColour(self.colorNames["A"]), "M": Preferences.getProjectBrowserColour(self.colorNames["M"]), "O": Preferences.getProjectBrowserColour(self.colorNames["O"]), "R": Preferences.getProjectBrowserColour(self.colorNames["R"]), "U": Preferences.getProjectBrowserColour(self.colorNames["U"]), "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]), } self.highLightColor = \ Preferences.getProjectBrowserColour("Highlighted") # needed by preferencesChanged() self.vcsStatusReport = {} def data(self, index, role): """ Public method to get data of an item. @param index index of the data to retrieve (QModelIndex) @param role role of data (Qt.ItemDataRole) @return requested data """ if not index.isValid(): return None if role == Qt.TextColorRole: if index.column() == 0: try: return index.internalPointer().getTextColor() except AttributeError: return None elif role == Qt.BackgroundColorRole: try: col = self.itemBackgroundColors[ index.internalPointer().vcsState] if col.isValid(): return col else: return None except AttributeError: return None except KeyError: return None return BrowserModel.data(self, index, role) def populateItem(self, parentItem, repopulate=False): """ Public method to populate an item's subtree. @param parentItem reference to the item to be populated @param repopulate flag indicating a repopulation (boolean) """ if parentItem.type() == ProjectBrowserItemSimpleDirectory: return # nothing to do elif parentItem.type() == ProjectBrowserItemDirectory: self.populateProjectDirectoryItem(parentItem, repopulate) elif parentItem.type() == ProjectBrowserItemFile: self.populateFileItem(parentItem, repopulate) else: BrowserModel.populateItem(self, parentItem, repopulate) def populateProjectDirectoryItem(self, parentItem, repopulate=False): """ Public method to populate a directory item's subtree. @param parentItem reference to the directory item to be populated @param repopulate flag indicating a repopulation (boolean) """ self._addWatchedItem(parentItem) qdir = QDir(parentItem.dirName()) if Preferences.getUI("BrowsersListHiddenFiles"): filter = QDir.Filters(QDir.AllEntries | QDir.Hidden | QDir.NoDotAndDotDot) else: filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot) entryInfoList = qdir.entryInfoList(filter) if len(entryInfoList) > 0: if repopulate: self.beginInsertRows( self.createIndex(parentItem.row(), 0, parentItem), 0, len(entryInfoList) - 1) states = {} if self.project.vcs is not None: for f in entryInfoList: fname = f.absoluteFilePath() states[os.path.normcase(fname)] = 0 dname = parentItem.dirName() self.project.vcs.clearStatusCache() states = self.project.vcs.vcsAllRegisteredStates(states, dname) for f in entryInfoList: if f.isDir(): node = ProjectBrowserDirectoryItem( parentItem, Utilities.toNativeSeparators(f.absoluteFilePath()), parentItem.getProjectTypes()[0], False) else: node = ProjectBrowserFileItem( parentItem, Utilities.toNativeSeparators(f.absoluteFilePath()), parentItem.getProjectTypes()[0]) if self.project.vcs is not None: fname = f.absoluteFilePath() if states[os.path.normcase(fname)] == \ self.project.vcs.canBeCommitted: node.addVcsStatus(self.project.vcs.vcsName()) self.project.clearStatusMonitorCachedState( f.absoluteFilePath()) else: node.addVcsStatus(self.tr("local")) self._addItem(node, parentItem) if repopulate: self.endInsertRows() def projectClosed(self): """ Public method called after a project has been closed. """ self.__vcsStatus = {} self.watchedItems = {} watchedDirs = self.watcher.directories() if watchedDirs: self.watcher.removePaths(watchedDirs) self.rootItem.removeChildren() self.beginResetModel() self.endResetModel() # reset the module parser cache Utilities.ModuleParser.resetParsedModules() def projectOpened(self): """ Public method used to populate the model after a project has been opened. """ self.__vcsStatus = {} states = {} keys = list(self.projectBrowserTypes.keys())[:] if self.project.vcs is not None: for key in keys: for fn in self.project.pdata[key]: states[os.path.normcase( os.path.join(self.project.ppath, fn))] = 0 self.project.vcs.clearStatusCache() states = self.project.vcs.vcsAllRegisteredStates( states, self.project.ppath) self.inRefresh = True for key in keys: # Show the entry in bold in the others browser to make it more # distinguishable if key == "OTHERS": bold = True else: bold = False if key == "SOURCES": sourceLanguage = self.project.pdata["PROGLANGUAGE"][0] else: sourceLanguage = "" for fn in self.project.pdata[key]: fname = os.path.join(self.project.ppath, fn) parentItem, dt = self.findParentItemByName( self.projectBrowserTypes[key], fn) if os.path.isdir(fname): itm = ProjectBrowserDirectoryItem( parentItem, fname, self.projectBrowserTypes[key], False, bold) else: itm = ProjectBrowserFileItem(parentItem, fname, self.projectBrowserTypes[key], False, bold, sourceLanguage=sourceLanguage) self._addItem(itm, parentItem) if self.project.vcs is not None: if states[os.path.normcase(fname)] == \ self.project.vcs.canBeCommitted: itm.addVcsStatus(self.project.vcs.vcsName()) else: itm.addVcsStatus(self.tr("local")) else: itm.addVcsStatus("") self.inRefresh = False self.beginResetModel() self.endResetModel() def findParentItemByName(self, type_, name, dontSplit=False): """ Public method to find an item given its name. <b>Note</b>: This method creates all necessary parent items, if they don't exist. @param type_ type of the item @param name name of the item (string) @param dontSplit flag indicating the name should not be split (boolean) @return reference to the item found and the new display name (string) """ if dontSplit: pathlist = [] pathlist.append(name) pathlist.append("ignore_me") else: pathlist = re.split(r'/|\\', name) if len(pathlist) > 1: olditem = self.rootItem path = self.project.ppath for p in pathlist[:-1]: itm = self.findChildItem(p, 0, olditem) path = os.path.join(path, p) if itm is None: itm = ProjectBrowserSimpleDirectoryItem( olditem, type_, p, path) self.__addVCSStatus(itm, path) if self.inRefresh: self._addItem(itm, olditem) else: if olditem == self.rootItem: oldindex = QModelIndex() else: oldindex = self.createIndex( olditem.row(), 0, olditem) self.addItem(itm, oldindex) else: if type_ and type_ not in itm.getProjectTypes(): itm.addProjectType(type_) index = self.createIndex(itm.row(), 0, itm) self.dataChanged.emit(index, index) olditem = itm return (itm, pathlist[-1]) else: return (self.rootItem, name) def findChildItem(self, text, column, parentItem=None): """ Public method to find a child item given some text. @param text text to search for (string) @param column column to search in (integer) @param parentItem reference to parent item @return reference to the item found """ if parentItem is None: parentItem = self.rootItem for itm in parentItem.children(): if itm.data(column) == text: return itm return None def addNewItem(self, typeString, name, additionalTypeStrings=[]): """ Public method to add a new item to the model. @param typeString string denoting the type of the new item (string) @param name name of the new item (string) @param additionalTypeStrings names of additional types (list of string) """ # Show the entry in bold in the others browser to make it more # distinguishable if typeString == "OTHERS": bold = True else: bold = False fname = os.path.join(self.project.ppath, name) parentItem, dt = self.findParentItemByName( self.projectBrowserTypes[typeString], name) if parentItem == self.rootItem: parentIndex = QModelIndex() else: parentIndex = self.createIndex(parentItem.row(), 0, parentItem) if os.path.isdir(fname): itm = ProjectBrowserDirectoryItem( parentItem, fname, self.projectBrowserTypes[typeString], False, bold) else: if typeString == "SOURCES": sourceLanguage = self.project.pdata["PROGLANGUAGE"][0] else: sourceLanguage = "" itm = ProjectBrowserFileItem(parentItem, fname, self.projectBrowserTypes[typeString], False, bold, sourceLanguage=sourceLanguage) self.__addVCSStatus(itm, fname) if additionalTypeStrings: for additionalTypeString in additionalTypeStrings: type_ = self.projectBrowserTypes[additionalTypeString] itm.addProjectType(type_) self.addItem(itm, parentIndex) def renameItem(self, name, newFilename): """ Public method to rename an item. @param name the old display name (string) @param newFilename new filename of the item (string) """ itm = self.findItem(name) if itm is None: return index = self.createIndex(itm.row(), 0, itm) itm.setName(newFilename) self.dataChanged.emit(index, index) self.repopulateItem(newFilename) def findItem(self, name): """ Public method to find an item given its name. @param name name of the item (string) @return reference to the item found """ if QDir.isAbsolutePath(name): name = self.project.getRelativePath(name) pathlist = re.split(r'/|\\', name) if len(pathlist) > 0: olditem = self.rootItem for p in pathlist: itm = self.findChildItem(p, 0, olditem) if itm is None: return None olditem = itm return itm else: return None def itemIndexByName(self, name): """ Public method to find an item's index given its name. @param name name of the item (string) @return index of the item found (QModelIndex) """ itm = self.findItem(name) if itm is None: index = QModelIndex() else: index = self.createIndex(itm.row(), 0, itm) return index def itemIndexByNameAndLine(self, name, lineno): """ Public method to find an item's index given its name. @param name name of the item (string) @param lineno one based line number of the item (integer) @return index of the item found (QModelIndex) """ index = QModelIndex() itm = self.findItem(name) if itm is not None and \ isinstance(itm, ProjectBrowserFileItem): olditem = itm autoPopulate = Preferences.getProject("AutoPopulateItems") while itm is not None: if not itm.isPopulated(): if itm.isLazyPopulated() and autoPopulate: self.populateItem(itm) else: break for child in itm.children(): try: start, end = child.boundaries() if end == -1: end = 1000000 # assume end of file if start <= lineno <= end: itm = child break except AttributeError: pass else: itm = None if itm: olditem = itm index = self.createIndex(olditem.row(), 0, olditem) return index def directoryChanged(self, path): """ Public slot to handle the directoryChanged signal of the watcher. @param path path of the directory (string) """ if path not in self.watchedItems: # just ignore the situation we don't have a reference to the item return if Preferences.getUI("BrowsersListHiddenFiles"): filter = QDir.Filters(QDir.AllEntries | QDir.Hidden | QDir.NoDotAndDotDot) else: filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot) for itm in self.watchedItems[path]: oldCnt = itm.childCount() qdir = QDir(itm.dirName()) entryInfoList = qdir.entryInfoList(filter) # step 1: check for new entries children = itm.children() for f in entryInfoList: fpath = Utilities.toNativeSeparators(f.absoluteFilePath()) childFound = False for child in children: if child.name() == fpath: childFound = True children.remove(child) break if childFound: continue cnt = itm.childCount() self.beginInsertRows(self.createIndex(itm.row(), 0, itm), cnt, cnt) if f.isDir(): node = ProjectBrowserDirectoryItem( itm, Utilities.toNativeSeparators(f.absoluteFilePath()), itm.getProjectTypes()[0], False) else: node = ProjectBrowserFileItem( itm, Utilities.toNativeSeparators(f.absoluteFilePath()), itm.getProjectTypes()[0]) self._addItem(node, itm) if self.project.vcs is not None: self.project.vcs.clearStatusCache() state = self.project.vcs.vcsRegisteredState(node.name()) if state == self.project.vcs.canBeCommitted: node.addVcsStatus(self.project.vcs.vcsName()) else: node.addVcsStatus(self.tr("local")) self.endInsertRows() # step 2: check for removed entries if len(entryInfoList) != itm.childCount(): for row in range(oldCnt - 1, -1, -1): child = itm.child(row) childname = Utilities.fromNativeSeparators(child.name()) entryFound = False for f in entryInfoList: if f.absoluteFilePath() == childname: entryFound = True entryInfoList.remove(f) break if entryFound: continue self._removeWatchedItem(child) self.beginRemoveRows(self.createIndex(itm.row(), 0, itm), row, row) itm.removeChild(child) self.endRemoveRows() def __addVCSStatus(self, item, name): """ Private method used to set the vcs status of a node. @param item item to work on @param name filename belonging to this item (string) """ if self.project.vcs is not None: state = self.project.vcs.vcsRegisteredState(name) if state == self.project.vcs.canBeCommitted: item.addVcsStatus(self.project.vcs.vcsName()) else: item.addVcsStatus(self.tr("local")) else: item.addVcsStatus("") def __updateVCSStatus(self, item, name, recursive=True): """ Private method used to update the vcs status of a node. @param item item to work on @param name filename belonging to this item (string) @keyparam recursive flag indicating a recursive update (boolean) """ if self.project.vcs is not None: self.project.vcs.clearStatusCache() state = self.project.vcs.vcsRegisteredState(name) if state == self.project.vcs.canBeCommitted: item.setVcsStatus(self.project.vcs.vcsName()) else: item.setVcsStatus(self.tr("local")) if recursive: name = os.path.dirname(name) parentItem = item.parent() if name and parentItem is not self.rootItem: self.__updateVCSStatus(parentItem, name, recursive) else: item.setVcsStatus("") index = self.createIndex(item.row(), 0, item) self.dataChanged.emit(index, index) def updateVCSStatus(self, name, recursive=True): """ Public method used to update the vcs status of a node. @param name filename belonging to this item (string) @param recursive flag indicating a recursive update (boolean) """ item = self.findItem(name) if item: self.__updateVCSStatus(item, name, recursive) def removeItem(self, name): """ Public method to remove a named item. @param name file or directory name of the item (string). """ fname = os.path.basename(name) parentItem = self.findParentItemByName(0, name)[0] if parentItem == self.rootItem: parentIndex = QModelIndex() else: parentIndex = self.createIndex(parentItem.row(), 0, parentItem) childItem = self.findChildItem(fname, 0, parentItem) if childItem is not None: self.beginRemoveRows(parentIndex, childItem.row(), childItem.row()) parentItem.removeChild(childItem) self.endRemoveRows() def repopulateItem(self, name): """ Public method to repopulate an item. @param name name of the file relative to the project root (string) """ itm = self.findItem(name) if itm is None: return if itm.isLazyPopulated(): if not itm.isPopulated(): # item is not populated yet, nothing to do return if itm.childCount(): index = self.createIndex(itm.row(), 0, itm) self.beginRemoveRows(index, 0, itm.childCount() - 1) itm.removeChildren() self.endRemoveRows() Utilities.ModuleParser.resetParsedModule( os.path.join(self.project.ppath, name)) self.populateItem(itm, True) def projectPropertiesChanged(self): """ Public method to react on a change of the project properties. """ # nothing to do for now return def changeVCSStates(self, statesList): """ Public slot to record the (non normal) VCS states. @param statesList list of VCS state entries (list of strings) giving the states in the first column and the path relative to the project directory starting with the third column. The allowed status flags are: <ul> <li>"A" path was added but not yet comitted</li> <li>"M" path has local changes</li> <li>"O" path was removed</li> <li>"R" path was deleted and then re-added</li> <li>"U" path needs an update</li> <li>"Z" path contains a conflict</li> <li>" " path is back at normal</li> </ul> """ statesList.sort() lastHead = "" itemCache = {} if len(statesList) == 1 and statesList[0] == '--RESET--': statesList = [] for name in list(self.__vcsStatus.keys()): statesList.append(" {0}".format(name)) for name in statesList: state = name[0] name = name[1:].strip() if state == ' ': if name in self.__vcsStatus: del self.__vcsStatus[name] else: self.__vcsStatus[name] = state try: itm = itemCache[name] except KeyError: itm = self.findItem(name) if itm: itemCache[name] = itm if itm: itm.setVcsState(state) itm.setVcsStatus(self.project.vcs.vcsName()) index1 = self.createIndex(itm.row(), 0, itm) index2 = self.createIndex(itm.row(), self.rootItem.columnCount(), itm) self.dataChanged.emit(index1, index2) head, tail = os.path.split(name) if head != lastHead: if lastHead: self.__changeParentsVCSState(lastHead, itemCache) lastHead = head if lastHead: self.__changeParentsVCSState(lastHead, itemCache) try: globalVcsStatus = sorted(self.__vcsStatus.values())[-1] except IndexError: globalVcsStatus = ' ' self.vcsStateChanged.emit(globalVcsStatus) def __changeParentsVCSState(self, path, itemCache): """ Private method to recursively change the parents VCS state. @param path pathname of parent item (string) @param itemCache reference to the item cache used to store references to named items """ while path: try: itm = itemCache[path] except KeyError: itm = self.findItem(path) if itm: itemCache[path] = itm if itm: state = " " for id_ in itm.children(): if state < id_.vcsState: state = id_.vcsState if state != itm.vcsState: itm.setVcsState(state) index1 = self.createIndex(itm.row(), 0, itm) index2 = self.createIndex(itm.row(), self.rootItem.columnCount(), itm) self.dataChanged.emit(index1, index2) path, tail = os.path.split(path) def preferencesChanged(self): """ Public method used to handle a change in preferences. """ for code in list(self.colorNames.keys()): color = Preferences.getProjectBrowserColour(self.colorNames[code]) if color.name() == self.itemBackgroundColors[code].name(): continue self.itemBackgroundColors[code] = color color = Preferences.getProjectBrowserColour("Highlighted") if self.highLightColor.name() != color.name(): self.highLightColor = color
class _FileWatcher(QObject): """File watcher. QFileSystemWatcher notifies client about any change (file access mode, modification date, etc.) But, we need signal, only after file contents had been changed """ modified = pyqtSignal(bool) removed = pyqtSignal(bool) def __init__(self, path): QObject.__init__(self) self._contents = None self._watcher = QFileSystemWatcher() self._timer = None self._path = path self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None self.setPath(path) self.enable() def term(self): self.disable() def enable(self): """Enable signals from the watcher """ self._watcher.fileChanged.connect(self._onFileChanged) def disable(self): """Disable signals from the watcher """ self._watcher.fileChanged.disconnect(self._onFileChanged) self._stopTimer() def setContents(self, contents): """Set file contents. Watcher uses it to compare old and new contents of the file. """ self._contents = contents # Qt File watcher may work incorrectly, if file was not existing, when it started if not self._watcher.files(): self.setPath(self._path) self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None def setPath(self, path): """Path had been changed or file had been created. Set new path """ if self._watcher.files(): self._watcher.removePaths(self._watcher.files()) if path is not None and os.path.isfile(path): self._watcher.addPath(path) self._path = path self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None def _emitModifiedStatus(self): """Emit self.modified signal with right status """ isModified = self._contents != self._safeRead(self._path) if isModified != self._lastEmittedModifiedStatus: self.modified.emit(isModified) self._lastEmittedModifiedStatus = isModified def _emitRemovedStatus(self, isRemoved): """Emit 'removed', if status changed""" if isRemoved != self._lastEmittedRemovedStatus: self._lastEmittedRemovedStatus = isRemoved self.removed.emit(isRemoved) @pyqtSlot() def _onFileChanged(self): """File changed. Emit own signal, if contents changed """ if os.path.exists(self._path): self._emitModifiedStatus() else: self._emitRemovedStatus(True) # Sometimes QFileSystemWatcher emits only 1 signal for 2 modifications # Check once more later self._startTimer() def _startTimer(self): """Init a timer. It is used for monitoring file after deletion. Git removes file, than restores it. """ if self._timer is None: self._timer = QTimer() self._timer.setInterval(500) self._timer.timeout.connect(self._onCheckIfDeletedTimer) self._timer.start() def _stopTimer(self): """Stop timer, if exists """ if self._timer is not None: self._timer.stop() @pyqtSlot() def _onCheckIfDeletedTimer(self): """Check, if file has been restored """ if os.path.exists(self._path): self.setPath(self._path) # restart Qt file watcher after file has been restored self._stopTimer() self._emitRemovedStatus(False) self._emitModifiedStatus() def _safeRead(self, path): """Read file. Ignore exceptions """ try: with open(path, 'rb') as file: return file.read() except (OSError, IOError): return None
class _FileWatcher(QObject): """File watcher. QFileSystemWatcher notifies client about any change (file access mode, modification date, etc.) But, we need signal, only after file contents had been changed """ modified = pyqtSignal(bool) removed = pyqtSignal(bool) def __init__(self, path): QObject.__init__(self) self._contents = None self._watcher = QFileSystemWatcher(self) self._timer = None self._path = path self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None self.setPath(path) self.enable() def term(self): self.disable() sip.delete(self) def enable(self): """Enable signals from the watcher """ self._watcher.fileChanged.connect(self._onFileChanged) def disable(self): """Disable signals from the watcher """ self._watcher.fileChanged.disconnect(self._onFileChanged) self._stopTimer() def setContents(self, contents): """Set file contents. Watcher uses it to compare old and new contents of the file. """ self._contents = contents # Qt File watcher may work incorrectly, if file was not existing, when it started if not self._watcher.files(): self.setPath(self._path) self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None def setPath(self, path): """Path had been changed or file had been created. Set new path """ if self._watcher.files(): self._watcher.removePaths(self._watcher.files()) if path is not None and os.path.isfile(path): self._watcher.addPath(path) self._path = path self._lastEmittedModifiedStatus = None self._lastEmittedRemovedStatus = None def _emitModifiedStatus(self): """Emit self.modified signal with right status """ isModified = self._contents != self._safeRead(self._path) if isModified != self._lastEmittedModifiedStatus: self.modified.emit(isModified) self._lastEmittedModifiedStatus = isModified def _emitRemovedStatus(self, isRemoved): """Emit 'removed', if status changed""" if isRemoved != self._lastEmittedRemovedStatus: self._lastEmittedRemovedStatus = isRemoved self.removed.emit(isRemoved) @pyqtSlot() def _onFileChanged(self): """File changed. Emit own signal, if contents changed """ if os.path.exists(self._path): self._emitModifiedStatus() else: self._emitRemovedStatus(True) # Sometimes QFileSystemWatcher emits only 1 signal for 2 modifications # Check once more later self._startTimer() def _startTimer(self): """Init a timer. It is used for monitoring file after deletion. Git removes file, than restores it. """ if self._timer is None: self._timer = QTimer() self._timer.setInterval(500) self._timer.timeout.connect(self._onCheckIfDeletedTimer) self._timer.start() def _stopTimer(self): """Stop timer, if exists """ if self._timer is not None: self._timer.stop() @pyqtSlot() def _onCheckIfDeletedTimer(self): """Check, if file has been restored """ if os.path.exists(self._path): self.setPath( self._path ) # restart Qt file watcher after file has been restored self._stopTimer() self._emitRemovedStatus(False) self._emitModifiedStatus() def _safeRead(self, path): """Read file. Ignore exceptions """ try: with open(path, 'rb') as file: return file.read() except (OSError, IOError): return None
class ExternalEditor(QObject): """Class to simplify editing a text in an external editor. Attributes: _text: The current text before the editor is opened. _filename: The name of the file to be edited. _remove_file: Whether the file should be removed when the editor is closed. _proc: The GUIProcess of the editor. _watcher: A QFileSystemWatcher to watch the edited file for changes. Only set if watch=True. _content: The last-saved text of the editor. Signals: file_updated: The text in the edited file was updated. arg: The new text. editing_finished: The editor process was closed. """ file_updated = pyqtSignal(str) editing_finished = pyqtSignal() def __init__(self, parent=None, watch=False): super().__init__(parent) self._filename = None self._proc = None self._remove_file = None self._watcher = QFileSystemWatcher(parent=self) if watch else None self._content = None def _cleanup(self): """Clean up temporary files after the editor closed.""" assert self._remove_file is not None watched_files = self._watcher.files() if self._watcher else [] if watched_files: failed = self._watcher.removePaths(watched_files) if failed: log.procs.error("Failed to unwatch paths: {}".format(failed)) if self._filename is None or not self._remove_file: # Could not create initial file. return try: if self._proc.exit_status() != QProcess.CrashExit: os.remove(self._filename) except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to delete tempfile... ({})".format(e)) @pyqtSlot(int, QProcess.ExitStatus) def _on_proc_closed(self, _exitcode, exitstatus): """Write the editor text into the form field and clean up tempfile. Callback for QProcess when the editor was closed. """ log.procs.debug("Editor closed") if exitstatus != QProcess.NormalExit: # No error/cleanup here, since we already handle this in # on_proc_error. return # do a final read to make sure we don't miss the last signal self._on_file_changed(self._filename) self.editing_finished.emit() self._cleanup() @pyqtSlot(QProcess.ProcessError) def _on_proc_error(self, _err): self._cleanup() def edit(self, text, caret_position=None): """Edit a given text. Args: text: The initial text to edit. caret_position: The position of the caret in the text. """ if self._filename is not None: raise ValueError("Already editing a file!") try: self._filename = self._create_tempfile(text, 'qutebrowser-editor-') except OSError as e: message.error("Failed to create initial file: {}".format(e)) return self._remove_file = True line, column = self._calc_line_and_column(text, caret_position) self._start_editor(line=line, column=column) def backup(self): """Create a backup if the content has changed from the original.""" if not self._content: return try: fname = self._create_tempfile(self._content, 'qutebrowser-editor-backup-') message.info('Editor backup at {}'.format(fname)) except OSError as e: message.error('Failed to create editor backup: {}'.format(e)) def _create_tempfile(self, text, prefix): # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update # the file from the external editor, see # https://github.com/qutebrowser/qutebrowser/issues/1767 with tempfile.NamedTemporaryFile( mode='w', prefix=prefix, encoding=config.val.editor.encoding, delete=False) as fobj: if text: fobj.write(text) return fobj.name @pyqtSlot(str) def _on_file_changed(self, path): try: with open(path, 'r', encoding=config.val.editor.encoding) as f: text = f.read() except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to read back edited file: {}".format(e)) return log.procs.debug("Read back: {}".format(text)) if self._content != text: self._content = text self.file_updated.emit(text) def edit_file(self, filename): """Edit the file with the given filename.""" self._filename = filename self._remove_file = False self._start_editor() def _start_editor(self, line=1, column=1): """Start the editor with the file opened as self._filename. Args: line: the line number to pass to the editor column: the column number to pass to the editor """ self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc.finished.connect(self._on_proc_closed) self._proc.error.connect(self._on_proc_error) editor = config.val.editor.command executable = editor[0] if self._watcher: ok = self._watcher.addPath(self._filename) if not ok: log.procs.error("Failed to watch path: {}" .format(self._filename)) self._watcher.fileChanged.connect(self._on_file_changed) args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args) def _calc_line_and_column(self, text, caret_position): r"""Calculate line and column numbers given a text and caret position. Both line and column are 1-based indexes, because that's what most editors use as line and column starting index. By "most" we mean at least vim, nvim, gvim, emacs, atom, sublimetext, notepad++, brackets, visual studio, QtCreator and so on. To find the line we just count how many newlines there are before the caret and add 1. To find the column we calculate the difference between the caret and the last newline before the caret. For example in the text `aaa\nbb|bbb` (| represents the caret): caret_position = 6 text[:caret_position] = `aaa\nbb` text[:caret_position].count('\n') = 1 caret_position - text[:caret_position].rfind('\n') = 3 Thus line, column = 2, 3, and the caret is indeed in the second line, third column Args: text: the text for which the numbers must be calculated caret_position: the position of the caret in the text, or None Return: A (line, column) tuple of (int, int) """ if caret_position is None: return 1, 1 line = text[:caret_position].count('\n') + 1 column = caret_position - text[:caret_position].rfind('\n') return line, column def _sub_placeholder(self, arg, line, column): """Substitute a single placeholder. If the `arg` input to this function is a valid placeholder it will be substituted with the appropriate value, otherwise it will be left unchanged. Args: arg: an argument of editor.command. line: the previously-calculated line number for the text caret. column: the previously-calculated column number for the text caret. Return: The substituted placeholder or the original argument. """ replacements = { '{}': self._filename, '{file}': self._filename, '{line}': str(line), '{line0}': str(line-1), '{column}': str(column), '{column0}': str(column-1) } for old, new in replacements.items(): arg = arg.replace(old, new) return arg
class ProjectBrowserModel(BrowserModel): """ Class implementing the project browser model. @signal vcsStateChanged(str) emitted after the VCS state has changed """ vcsStateChanged = pyqtSignal(str) def __init__(self, parent): """ Constructor @param parent reference to parent object (Project.Project) """ super(ProjectBrowserModel, self).__init__(parent, nopopulate=True) rootData = self.tr("Name") self.rootItem = BrowserItem(None, rootData) self.rootItem.itemData.append(self.tr("VCS Status")) self.progDir = None self.project = parent self.watchedItems = {} self.watcher = QFileSystemWatcher(self) self.watcher.directoryChanged.connect(self.directoryChanged) self.inRefresh = False self.projectBrowserTypes = { "SOURCES": ProjectBrowserSourceType, "FORMS": ProjectBrowserFormType, "RESOURCES": ProjectBrowserResourceType, "INTERFACES": ProjectBrowserInterfaceType, "TRANSLATIONS": ProjectBrowserTranslationType, "OTHERS": ProjectBrowserOthersType, } self.colorNames = { "A": "VcsAdded", "M": "VcsModified", "O": "VcsRemoved", "R": "VcsReplaced", "U": "VcsUpdate", "Z": "VcsConflict", } self.itemBackgroundColors = { " ": QColor(), "A": Preferences.getProjectBrowserColour(self.colorNames["A"]), "M": Preferences.getProjectBrowserColour(self.colorNames["M"]), "O": Preferences.getProjectBrowserColour(self.colorNames["O"]), "R": Preferences.getProjectBrowserColour(self.colorNames["R"]), "U": Preferences.getProjectBrowserColour(self.colorNames["U"]), "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]), } self.highLightColor = \ Preferences.getProjectBrowserColour("Highlighted") # needed by preferencesChanged() self.vcsStatusReport = {} def data(self, index, role): """ Public method to get data of an item. @param index index of the data to retrieve (QModelIndex) @param role role of data (Qt.ItemDataRole) @return requested data """ if not index.isValid(): return None if role == Qt.TextColorRole: if index.column() == 0: try: return index.internalPointer().getTextColor() except AttributeError: return None elif role == Qt.BackgroundColorRole: try: col = self.itemBackgroundColors[ index.internalPointer().vcsState] if col.isValid(): return col else: return None except AttributeError: return None except KeyError: return None return BrowserModel.data(self, index, role) def populateItem(self, parentItem, repopulate=False): """ Public method to populate an item's subtree. @param parentItem reference to the item to be populated @param repopulate flag indicating a repopulation (boolean) """ if parentItem.type() == ProjectBrowserItemSimpleDirectory: return # nothing to do elif parentItem.type() == ProjectBrowserItemDirectory: self.populateProjectDirectoryItem(parentItem, repopulate) elif parentItem.type() == ProjectBrowserItemFile: self.populateFileItem(parentItem, repopulate) else: BrowserModel.populateItem(self, parentItem, repopulate) def populateProjectDirectoryItem(self, parentItem, repopulate=False): """ Public method to populate a directory item's subtree. @param parentItem reference to the directory item to be populated @param repopulate flag indicating a repopulation (boolean) """ self._addWatchedItem(parentItem) qdir = QDir(parentItem.dirName()) if Preferences.getUI("BrowsersListHiddenFiles"): filter = QDir.Filters(QDir.AllEntries | QDir.Hidden | QDir.NoDotAndDotDot) else: filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot) entryInfoList = qdir.entryInfoList(filter) if len(entryInfoList) > 0: if repopulate: self.beginInsertRows(self.createIndex( parentItem.row(), 0, parentItem), 0, len(entryInfoList) - 1) states = {} if self.project.vcs is not None: for f in entryInfoList: fname = f.absoluteFilePath() states[os.path.normcase(fname)] = 0 dname = parentItem.dirName() self.project.vcs.clearStatusCache() states = self.project.vcs.vcsAllRegisteredStates(states, dname) for f in entryInfoList: if f.isDir(): node = ProjectBrowserDirectoryItem( parentItem, Utilities.toNativeSeparators(f.absoluteFilePath()), parentItem.getProjectTypes()[0], False) else: node = ProjectBrowserFileItem( parentItem, Utilities.toNativeSeparators(f.absoluteFilePath()), parentItem.getProjectTypes()[0]) if self.project.vcs is not None: fname = f.absoluteFilePath() if states[os.path.normcase(fname)] == \ self.project.vcs.canBeCommitted: node.addVcsStatus(self.project.vcs.vcsName()) self.project.clearStatusMonitorCachedState( f.absoluteFilePath()) else: node.addVcsStatus(self.tr("local")) self._addItem(node, parentItem) if repopulate: self.endInsertRows() def projectClosed(self): """ Public method called after a project has been closed. """ self.__vcsStatus = {} self.watchedItems = {} watchedDirs = self.watcher.directories() if watchedDirs: self.watcher.removePaths(watchedDirs) self.rootItem.removeChildren() self.beginResetModel() self.endResetModel() # reset the module parser cache Utilities.ModuleParser.resetParsedModules() def projectOpened(self): """ Public method used to populate the model after a project has been opened. """ self.__vcsStatus = {} states = {} keys = list(self.projectBrowserTypes.keys())[:] if self.project.vcs is not None: for key in keys: for fn in self.project.pdata[key]: states[os.path.normcase( os.path.join(self.project.ppath, fn))] = 0 self.project.vcs.clearStatusCache() states = self.project.vcs.vcsAllRegisteredStates( states, self.project.ppath) self.inRefresh = True for key in keys: # Show the entry in bold in the others browser to make it more # distinguishable if key == "OTHERS": bold = True else: bold = False if key == "SOURCES": sourceLanguage = self.project.pdata["PROGLANGUAGE"][0] else: sourceLanguage = "" for fn in self.project.pdata[key]: fname = os.path.join(self.project.ppath, fn) parentItem, dt = self.findParentItemByName( self.projectBrowserTypes[key], fn) if os.path.isdir(fname): itm = ProjectBrowserDirectoryItem( parentItem, fname, self.projectBrowserTypes[key], False, bold) else: itm = ProjectBrowserFileItem( parentItem, fname, self.projectBrowserTypes[key], False, bold, sourceLanguage=sourceLanguage) self._addItem(itm, parentItem) if self.project.vcs is not None: if states[os.path.normcase(fname)] == \ self.project.vcs.canBeCommitted: itm.addVcsStatus(self.project.vcs.vcsName()) else: itm.addVcsStatus(self.tr("local")) else: itm.addVcsStatus("") self.inRefresh = False self.beginResetModel() self.endResetModel() def findParentItemByName(self, type_, name, dontSplit=False): """ Public method to find an item given its name. <b>Note</b>: This method creates all necessary parent items, if they don't exist. @param type_ type of the item @param name name of the item (string) @param dontSplit flag indicating the name should not be split (boolean) @return reference to the item found and the new display name (string) """ if dontSplit: pathlist = [] pathlist.append(name) pathlist.append("ignore_me") else: pathlist = re.split(r'/|\\', name) if len(pathlist) > 1: olditem = self.rootItem path = self.project.ppath for p in pathlist[:-1]: itm = self.findChildItem(p, 0, olditem) path = os.path.join(path, p) if itm is None: itm = ProjectBrowserSimpleDirectoryItem( olditem, type_, p, path) self.__addVCSStatus(itm, path) if self.inRefresh: self._addItem(itm, olditem) else: if olditem == self.rootItem: oldindex = QModelIndex() else: oldindex = self.createIndex( olditem.row(), 0, olditem) self.addItem(itm, oldindex) else: if type_ and type_ not in itm.getProjectTypes(): itm.addProjectType(type_) index = self.createIndex(itm.row(), 0, itm) self.dataChanged.emit(index, index) olditem = itm return (itm, pathlist[-1]) else: return (self.rootItem, name) def findChildItem(self, text, column, parentItem=None): """ Public method to find a child item given some text. @param text text to search for (string) @param column column to search in (integer) @param parentItem reference to parent item @return reference to the item found """ if parentItem is None: parentItem = self.rootItem for itm in parentItem.children(): if itm.data(column) == text: return itm return None def addNewItem(self, typeString, name, additionalTypeStrings=[]): """ Public method to add a new item to the model. @param typeString string denoting the type of the new item (string) @param name name of the new item (string) @param additionalTypeStrings names of additional types (list of string) """ # Show the entry in bold in the others browser to make it more # distinguishable if typeString == "OTHERS": bold = True else: bold = False fname = os.path.join(self.project.ppath, name) parentItem, dt = self.findParentItemByName( self.projectBrowserTypes[typeString], name) if parentItem == self.rootItem: parentIndex = QModelIndex() else: parentIndex = self.createIndex(parentItem.row(), 0, parentItem) if os.path.isdir(fname): itm = ProjectBrowserDirectoryItem( parentItem, fname, self.projectBrowserTypes[typeString], False, bold) else: if typeString == "SOURCES": sourceLanguage = self.project.pdata["PROGLANGUAGE"][0] else: sourceLanguage = "" itm = ProjectBrowserFileItem( parentItem, fname, self.projectBrowserTypes[typeString], False, bold, sourceLanguage=sourceLanguage) self.__addVCSStatus(itm, fname) if additionalTypeStrings: for additionalTypeString in additionalTypeStrings: type_ = self.projectBrowserTypes[additionalTypeString] itm.addProjectType(type_) self.addItem(itm, parentIndex) def renameItem(self, name, newFilename): """ Public method to rename an item. @param name the old display name (string) @param newFilename new filename of the item (string) """ itm = self.findItem(name) if itm is None: return index = self.createIndex(itm.row(), 0, itm) itm.setName(newFilename) self.dataChanged.emit(index, index) self.repopulateItem(newFilename) def findItem(self, name): """ Public method to find an item given its name. @param name name of the item (string) @return reference to the item found """ if QDir.isAbsolutePath(name): name = self.project.getRelativePath(name) pathlist = re.split(r'/|\\', name) if len(pathlist) > 0: olditem = self.rootItem for p in pathlist: itm = self.findChildItem(p, 0, olditem) if itm is None: return None olditem = itm return itm else: return None def itemIndexByName(self, name): """ Public method to find an item's index given its name. @param name name of the item (string) @return index of the item found (QModelIndex) """ itm = self.findItem(name) if itm is None: index = QModelIndex() else: index = self.createIndex(itm.row(), 0, itm) return index def itemIndexByNameAndLine(self, name, lineno): """ Public method to find an item's index given its name. @param name name of the item (string) @param lineno one based line number of the item (integer) @return index of the item found (QModelIndex) """ index = QModelIndex() itm = self.findItem(name) if itm is not None and \ isinstance(itm, ProjectBrowserFileItem): olditem = itm autoPopulate = Preferences.getProject("AutoPopulateItems") while itm is not None: if not itm.isPopulated(): if itm.isLazyPopulated() and autoPopulate: self.populateItem(itm) else: break for child in itm.children(): try: start, end = child.boundaries() if end == -1: end = 1000000 # assume end of file if start <= lineno <= end: itm = child break except AttributeError: pass else: itm = None if itm: olditem = itm index = self.createIndex(olditem.row(), 0, olditem) return index def directoryChanged(self, path): """ Public slot to handle the directoryChanged signal of the watcher. @param path path of the directory (string) """ if path not in self.watchedItems: # just ignore the situation we don't have a reference to the item return if Preferences.getUI("BrowsersListHiddenFiles"): filter = QDir.Filters(QDir.AllEntries | QDir.Hidden | QDir.NoDotAndDotDot) else: filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot) for itm in self.watchedItems[path]: oldCnt = itm.childCount() qdir = QDir(itm.dirName()) entryInfoList = qdir.entryInfoList(filter) # step 1: check for new entries children = itm.children() for f in entryInfoList: fpath = Utilities.toNativeSeparators(f.absoluteFilePath()) childFound = False for child in children: if child.name() == fpath: childFound = True children.remove(child) break if childFound: continue cnt = itm.childCount() self.beginInsertRows( self.createIndex(itm.row(), 0, itm), cnt, cnt) if f.isDir(): node = ProjectBrowserDirectoryItem( itm, Utilities.toNativeSeparators(f.absoluteFilePath()), itm.getProjectTypes()[0], False) else: node = ProjectBrowserFileItem( itm, Utilities.toNativeSeparators(f.absoluteFilePath()), itm.getProjectTypes()[0]) self._addItem(node, itm) if self.project.vcs is not None: self.project.vcs.clearStatusCache() state = self.project.vcs.vcsRegisteredState(node.name()) if state == self.project.vcs.canBeCommitted: node.addVcsStatus(self.project.vcs.vcsName()) else: node.addVcsStatus(self.tr("local")) self.endInsertRows() # step 2: check for removed entries if len(entryInfoList) != itm.childCount(): for row in range(oldCnt - 1, -1, -1): child = itm.child(row) childname = Utilities.fromNativeSeparators(child.name()) entryFound = False for f in entryInfoList: if f.absoluteFilePath() == childname: entryFound = True entryInfoList.remove(f) break if entryFound: continue self._removeWatchedItem(child) self.beginRemoveRows( self.createIndex(itm.row(), 0, itm), row, row) itm.removeChild(child) self.endRemoveRows() def __addVCSStatus(self, item, name): """ Private method used to set the vcs status of a node. @param item item to work on @param name filename belonging to this item (string) """ if self.project.vcs is not None: state = self.project.vcs.vcsRegisteredState(name) if state == self.project.vcs.canBeCommitted: item.addVcsStatus(self.project.vcs.vcsName()) else: item.addVcsStatus(self.tr("local")) else: item.addVcsStatus("") def __updateVCSStatus(self, item, name, recursive=True): """ Private method used to update the vcs status of a node. @param item item to work on @param name filename belonging to this item (string) @keyparam recursive flag indicating a recursive update (boolean) """ if self.project.vcs is not None: self.project.vcs.clearStatusCache() state = self.project.vcs.vcsRegisteredState(name) if state == self.project.vcs.canBeCommitted: item.setVcsStatus(self.project.vcs.vcsName()) else: item.setVcsStatus(self.tr("local")) if recursive: name = os.path.dirname(name) parentItem = item.parent() if name and parentItem is not self.rootItem: self.__updateVCSStatus(parentItem, name, recursive) else: item.setVcsStatus("") index = self.createIndex(item.row(), 0, item) self.dataChanged.emit(index, index) def updateVCSStatus(self, name, recursive=True): """ Public method used to update the vcs status of a node. @param name filename belonging to this item (string) @param recursive flag indicating a recursive update (boolean) """ item = self.findItem(name) if item: self.__updateVCSStatus(item, name, recursive) def removeItem(self, name): """ Public method to remove a named item. @param name file or directory name of the item (string). """ fname = os.path.basename(name) parentItem = self.findParentItemByName(0, name)[0] if parentItem == self.rootItem: parentIndex = QModelIndex() else: parentIndex = self.createIndex(parentItem.row(), 0, parentItem) childItem = self.findChildItem(fname, 0, parentItem) if childItem is not None: self.beginRemoveRows(parentIndex, childItem.row(), childItem.row()) parentItem.removeChild(childItem) self.endRemoveRows() def repopulateItem(self, name): """ Public method to repopulate an item. @param name name of the file relative to the project root (string) """ itm = self.findItem(name) if itm is None: return if itm.isLazyPopulated(): if not itm.isPopulated(): # item is not populated yet, nothing to do return if itm.childCount(): index = self.createIndex(itm.row(), 0, itm) self.beginRemoveRows(index, 0, itm.childCount() - 1) itm.removeChildren() self.endRemoveRows() Utilities.ModuleParser.resetParsedModule( os.path.join(self.project.ppath, name)) self.populateItem(itm, True) def projectPropertiesChanged(self): """ Public method to react on a change of the project properties. """ # nothing to do for now return def changeVCSStates(self, statesList): """ Public slot to record the (non normal) VCS states. @param statesList list of VCS state entries (list of strings) giving the states in the first column and the path relative to the project directory starting with the third column. The allowed status flags are: <ul> <li>"A" path was added but not yet comitted</li> <li>"M" path has local changes</li> <li>"O" path was removed</li> <li>"R" path was deleted and then re-added</li> <li>"U" path needs an update</li> <li>"Z" path contains a conflict</li> <li>" " path is back at normal</li> </ul> """ statesList.sort() lastHead = "" itemCache = {} if len(statesList) == 1 and statesList[0] == '--RESET--': statesList = [] for name in list(self.__vcsStatus.keys()): statesList.append(" {0}".format(name)) for name in statesList: state = name[0] name = name[1:].strip() if state == ' ': if name in self.__vcsStatus: del self.__vcsStatus[name] else: self.__vcsStatus[name] = state try: itm = itemCache[name] except KeyError: itm = self.findItem(name) if itm: itemCache[name] = itm if itm: itm.setVcsState(state) itm.setVcsStatus(self.project.vcs.vcsName()) index1 = self.createIndex(itm.row(), 0, itm) index2 = self.createIndex( itm.row(), self.rootItem.columnCount(), itm) self.dataChanged.emit(index1, index2) head, tail = os.path.split(name) if head != lastHead: if lastHead: self.__changeParentsVCSState(lastHead, itemCache) lastHead = head if lastHead: self.__changeParentsVCSState(lastHead, itemCache) try: globalVcsStatus = sorted(self.__vcsStatus.values())[-1] except IndexError: globalVcsStatus = ' ' self.vcsStateChanged.emit(globalVcsStatus) def __changeParentsVCSState(self, path, itemCache): """ Private method to recursively change the parents VCS state. @param path pathname of parent item (string) @param itemCache reference to the item cache used to store references to named items """ while path: try: itm = itemCache[path] except KeyError: itm = self.findItem(path) if itm: itemCache[path] = itm if itm: state = " " for id_ in itm.children(): if state < id_.vcsState: state = id_.vcsState if state != itm.vcsState: itm.setVcsState(state) index1 = self.createIndex(itm.row(), 0, itm) index2 = self.createIndex( itm.row(), self.rootItem.columnCount(), itm) self.dataChanged.emit(index1, index2) path, tail = os.path.split(path) def preferencesChanged(self): """ Public method used to handle a change in preferences. """ for code in list(self.colorNames.keys()): color = Preferences.getProjectBrowserColour(self.colorNames[code]) if color.name() == self.itemBackgroundColors[code].name(): continue self.itemBackgroundColors[code] = color color = Preferences.getProjectBrowserColour("Highlighted") if self.highLightColor.name() != color.name(): self.highLightColor = color
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupUi(self) self.centralwidget.hide() self.parser = argparse.ArgumentParser("playground") self.parser.add_argument("-d", "--data-dir", type=str, help="data dir") self.parser.add_argument("-a", "--console-address", type=str, help="console address", default='localhost') self.parser.add_argument("-p", "--console-port", type=int, help="console port", default=2222) self.args = self.parser.parse_args() self.data_dir = self.args.data_dir self.console_port = self.args.console_port self.console_address = self.args.console_address self.project = CetechProject() self.api = QtConsoleAPI(self.console_address, self.console_port) self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North) self.script_editor_widget = ScriptEditor(project_manager=self.project, api=self.api) self.script_editor_dock_widget = QDockWidget(self) self.script_editor_dock_widget.setWindowTitle("Script editor") self.script_editor_dock_widget.hide() self.script_editor_dock_widget.setFeatures(QDockWidget.AllDockWidgetFeatures) self.script_editor_dock_widget.setWidget(self.script_editor_widget) self.addDockWidget(Qt.TopDockWidgetArea, self.script_editor_dock_widget) self.log_widget = LogWidget(self.api, self.script_editor_widget) self.log_dock_widget = QDockWidget(self) self.log_dock_widget.hide() self.log_dock_widget.setWindowTitle("Log") self.log_dock_widget.setWidget(self.log_widget) self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock_widget) self.assetb_widget = AssetBrowser() self.assetb_dock_widget = QDockWidget(self) self.assetb_dock_widget.hide() self.assetb_dock_widget.setWindowTitle("Asset browser") self.assetb_dock_widget.setFeatures(QDockWidget.AllDockWidgetFeatures) self.assetb_dock_widget.setWidget(self.assetb_widget) self.addDockWidget(Qt.LeftDockWidgetArea, self.assetb_dock_widget) self.recorded_event_widget = RecordEventWidget(api=self.api) self.recorded_event_dock_widget = QDockWidget(self) self.recorded_event_dock_widget.setWindowTitle("Recorded events") self.recorded_event_dock_widget.hide() self.recorded_event_dock_widget.setFeatures(QDockWidget.AllDockWidgetFeatures) self.recorded_event_dock_widget.setWidget(self.recorded_event_widget) self.addDockWidget(Qt.RightDockWidgetArea, self.recorded_event_dock_widget) #TODO bug #114 workaround. Disable create sub engine... if platform.system().lower() != 'darwin': self.ogl_widget = CetechWidget(self, self.api) self.ogl_dock = QDockWidget(self) self.ogl_dock.hide() self.ogl_dock.setWidget(self.ogl_widget) self.addDockWidget(Qt.TopDockWidgetArea, self.ogl_dock) self.tabifyDockWidget(self.assetb_dock_widget, self.log_dock_widget) self.assetb_widget.asset_clicked.connect(self.open_asset) self.file_watch = QFileSystemWatcher(self) self.file_watch.fileChanged.connect(self.file_changed) self.file_watch.directoryChanged.connect(self.dir_changed) self.build_file_watch = QFileSystemWatcher(self) self.build_file_watch.fileChanged.connect(self.build_file_changed) self.build_file_watch.directoryChanged.connect(self.build_dir_changed) def open_asset(self, path, ext): if self.script_editor_widget.support_ext(ext): self.script_editor_widget.open_file(path) self.script_editor_dock_widget.show() self.script_editor_dock_widget.focusWidget() def open_project(self, name, dir): self.project.open_project(name, dir) # self.project.run_cetech(build_type=CetechProject.BUILD_DEBUG, compile=True, continu=True, daemon=True) if platform.system().lower() == 'darwin': wid = None else: wid = self.ogl_widget.winId() self.project.run_cetech(build_type=CetechProject.BUILD_DEBUG, compile_=True, continue_=True, wid=wid) self.api.start(QThread.LowPriority) self.assetb_widget.open_project(self.project.project_dir) self.assetb_dock_widget.show() self.log_dock_widget.show() #TODO bug #114 workaround. Disable create sub engine... if platform.system().lower() != 'darwin': self.ogl_dock.show() self.watch_project_dir() def watch_project_dir(self): files = self.file_watch.files() directories = self.file_watch.directories() if len(files): self.file_watch.removePaths(files) if len(directories): self.file_watch.removePaths(directories) files = self.build_file_watch.files() directories = self.build_file_watch.directories() if len(files): self.build_file_watch.removePaths(files) if len(directories): self.build_file_watch.removePaths(directories) files = [] it = QDirIterator(self.project.source_dir, QDirIterator.Subdirectories) while it.hasNext(): files.append(it.next()) self.file_watch.addPaths(files) files = [] it = QDirIterator(self.project.build_dir, QDirIterator.Subdirectories) while it.hasNext(): files.append(it.next()) self.build_file_watch.addPaths(files) def file_changed(self, path): self.api.compile_all() def dir_changed(self, path): self.watch_project_dir() def build_file_changed(self, path): self.api.autocomplete_list() def build_dir_changed(self, path): pass def open_script_editor(self): self.script_editor_dock_widget.show() def open_recorded_events(self): self.recorded_event_dock_widget.show() def closeEvent(self, evnt): self.api.disconnect() self.project.killall_process() self.statusbar.showMessage("Disconnecting ...") while self.api.connected: self.api.tick() self.statusbar.showMessage("Disconnected") evnt.accept()
class ExecuteOptionsPlugin(QWidget, Plugin): """ Handles setting the various arguments for running. Signals: executableChanged(str): Path of the new executable is emitted when changed executableInfoChanged(ExecutableInfo): Emitted when the executable path is changed workingDirChanged(str): Path of the current directory is changed """ executableChanged = pyqtSignal(str) executableInfoChanged = pyqtSignal(ExecutableInfo) workingDirChanged = pyqtSignal(str) useTestObjectsChanged = pyqtSignal(bool) def __init__(self, **kwds): super(ExecuteOptionsPlugin, self).__init__(**kwds) self._preferences.addInt("execute/maxRecentWorkingDirs", "Max recent working directories", 10, 1, 50, "Set the maximum number of recent working directories that have been used.", ) self._preferences.addInt("execute/maxRecentExes", "Max recent executables", 10, 1, 50, "Set the maximum number of recent executables that have been used.", ) self._preferences.addInt("execute/maxRecentArgs", "Max recent command line arguments", 10, 1, 50, "Set the maximum number of recent command line arguments that have been used.", ) self._preferences.addBool("execute/allowTestObjects", "Allow using test objects", False, "Allow using test objects by default", ) self._preferences.addBool("execute/mpiEnabled", "Enable MPI by default", False, "Set the MPI checkbox on by default", ) self._preferences.addString("execute/mpiArgs", "Default mpi command", "mpiexec -n 2", "Set the default MPI command to run", ) self._preferences.addBool("execute/threadsEnabled", "Enable threads by default", False, "Set the threads checkbox on by default", ) self._preferences.addString("execute/threadsArgs", "Default threads arguments", "--n-threads=2", "Set the default threads arguments", ) self.all_exe_layout = WidgetUtils.addLayout(grid=True) self.setLayout(self.all_exe_layout) self.working_label = WidgetUtils.addLabel(None, self, "Working Directory") self.all_exe_layout.addWidget(self.working_label, 0, 0) self.choose_working_button = WidgetUtils.addButton(None, self, "Choose", self._chooseWorkingDir) self.all_exe_layout.addWidget(self.choose_working_button, 0, 1) self.working_line = WidgetUtils.addLineEdit(None, self, None, readonly=True) self.working_line.setText(os.getcwd()) self.all_exe_layout.addWidget(self.working_line, 0, 2) self.exe_label = WidgetUtils.addLabel(None, self, "Executable") self.all_exe_layout.addWidget(self.exe_label, 1, 0) self.choose_exe_button = WidgetUtils.addButton(None, self, "Choose", self._chooseExecutable) self.all_exe_layout.addWidget(self.choose_exe_button, 1, 1) self.exe_line = WidgetUtils.addLineEdit(None, self, None, readonly=True) self.all_exe_layout.addWidget(self.exe_line, 1, 2) self.args_label = WidgetUtils.addLabel(None, self, "Extra Arguments") self.all_exe_layout.addWidget(self.args_label, 2, 0) self.args_line = WidgetUtils.addLineEdit(None, self, None) self.all_exe_layout.addWidget(self.args_line, 2, 2) self.test_label = WidgetUtils.addLabel(None, self, "Allow test objects") self.all_exe_layout.addWidget(self.test_label, 3, 0) self.test_checkbox = WidgetUtils.addCheckbox(None, self, "", self._allowTestObjects) self.test_checkbox.setChecked(self._preferences.value("execute/allowTestObjects")) self.all_exe_layout.addWidget(self.test_checkbox, 3, 1, alignment=Qt.AlignHCenter) self.mpi_label = WidgetUtils.addLabel(None, self, "Use MPI") self.all_exe_layout.addWidget(self.mpi_label, 4, 0) self.mpi_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.mpi_checkbox.setChecked(self._preferences.value("execute/mpiEnabled")) self.all_exe_layout.addWidget(self.mpi_checkbox, 4, 1, alignment=Qt.AlignHCenter) self.mpi_line = WidgetUtils.addLineEdit(None, self, None) self.mpi_line.setText(self._preferences.value("execute/mpiArgs")) self.mpi_line.cursorPositionChanged.connect(self._mpiLineCursorChanged) self.all_exe_layout.addWidget(self.mpi_line, 4, 2) self.threads_label = WidgetUtils.addLabel(None, self, "Use Threads") self.all_exe_layout.addWidget(self.threads_label, 5, 0) self.threads_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.threads_checkbox.setChecked(self._preferences.value("execute/threadsEnabled")) self.all_exe_layout.addWidget(self.threads_checkbox, 5, 1, alignment=Qt.AlignHCenter) self.threads_line = WidgetUtils.addLineEdit(None, self, None) self.threads_line.setText(self._preferences.value("execute/threadsArgs")) self.threads_line.cursorPositionChanged.connect(self._threadsLineCursorChanged) self.all_exe_layout.addWidget(self.threads_line, 5, 2) self.csv_label = WidgetUtils.addLabel(None, self, "Postprocessor CSV Output") self.all_exe_layout.addWidget(self.csv_label, 6, 0) self.csv_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.all_exe_layout.addWidget(self.csv_checkbox, 6, 1, alignment=Qt.AlignHCenter) self.csv_checkbox.setCheckState(Qt.Checked) self.recover_label = WidgetUtils.addLabel(None, self, "Recover") self.all_exe_layout.addWidget(self.recover_label, 7, 0) self.recover_checkbox = WidgetUtils.addCheckbox(None, self, "", None) self.all_exe_layout.addWidget(self.recover_checkbox, 7, 1, alignment=Qt.AlignHCenter) self._recent_exe_menu = None self._recent_working_menu = None self._recent_args_menu = None self._exe_watcher = QFileSystemWatcher() self._exe_watcher.fileChanged.connect(self.setExecutablePath) self._loading_dialog = QMessageBox(parent=self) self._loading_dialog.setWindowTitle("Loading executable") self._loading_dialog.setStandardButtons(QMessageBox.NoButton) # get rid of the OK button self._loading_dialog.setWindowModality(Qt.ApplicationModal) self._loading_dialog.setIcon(QMessageBox.Information) self._loading_dialog.setText("Loading executable") self.setup() def setExecutablePath(self, app_path): """ The user select a new executable path. Input: app_path: The path of the executable. """ if not app_path: return self._loading_dialog.setInformativeText(app_path) self._loading_dialog.show() self._loading_dialog.raise_() QApplication.processEvents() app_info = ExecutableInfo() app_info.setPath(app_path, self.test_checkbox.isChecked()) QApplication.processEvents() if app_info.valid(): self.exe_line.setText(app_path) self.executableInfoChanged.emit(app_info) self.executableChanged.emit(app_path) files = self._exe_watcher.files() if files: self._exe_watcher.removePaths(files) self._exe_watcher.addPath(app_path) self._updateRecentExe(app_path, not app_info.valid()) self._loading_dialog.hide() def _chooseExecutable(self): """ Open a dialog to allow the user to choose an executable. """ #FIXME: QFileDialog seems to be a bit broken. Using # .setFilter() to filter only executable files doesn't # seem to work. Setting a QSortFilterProxyModel doesn't # seem to work either. # So just use the static method. exe_name, other = QFileDialog.getOpenFileName(self, "Chooose executable") self.setExecutablePath(exe_name) def _allowTestObjects(self): """ Reload the ExecutableInfo based on whether we are allowing test objects or not. """ self.useTestObjectsChanged.emit(self.test_checkbox.isChecked()) self.setExecutablePath(self.exe_line.text()) def _workingDirChanged(self): """ Slot called when working directory changed. """ working = str(self.working_line.text()) self.setWorkingDir(working) def _chooseWorkingDir(self): """ Open dialog to choose a current working directory. """ dirname = QFileDialog.getExistingDirectory(self, "Choose directory") self.setWorkingDir(dirname) def setWorkingDir(self, dir_name): """ Sets the working directory. Input: dir_name: The path of the working directory. """ if not dir_name: return old_dirname = str(self.working_line.text()) try: os.chdir(dir_name) self.working_line.setText(dir_name) if old_dirname != dir_name: self.workingDirChanged.emit(dir_name) self._updateRecentWorkingDir(dir_name) except OSError: mooseutils.mooseError("Invalid directory %s" % dir_name, dialog=True) self._updateRecentWorkingDir(dir_name, True) def _setExecutableArgs(self, args): """ Set the executable arguments. Input: args: str: A string of all the arguments. """ self.args_line.setText(args) def buildCommand(self, input_file): cmd, args = self.buildCommandWithNoInputFile() args.append("-i") args.append(os.path.relpath(input_file)) return cmd, args def buildCommandWithNoInputFile(self): """ Builds the full command line with arguments. Return: <string of command to run>, <list of arguments> """ cmd = "" args = [] if self.mpi_checkbox.isChecked(): mpi_args = shlex.split(str(self.mpi_line.text())) if mpi_args: cmd = mpi_args[0] args = mpi_args[1:] args.append(str(self.exe_line.text())) if not cmd: cmd = str(self.exe_line.text()) args += shlex.split(str(self.args_line.text())) if self.recover_checkbox.isChecked(): args.append("--recover") if self.csv_checkbox.isChecked(): #args.append("--no-color") args.append("Outputs/csv=true") if self.threads_checkbox.isChecked(): args += shlex.split(str(self.threads_line.text())) return cmd, args def _updateRecentExe(self, path, remove=False): """ Updates the recently used menu with the current executable """ if self._recent_exe_menu: abs_path = os.path.normcase(os.path.abspath(path)) if remove: self._recent_exe_menu.removeEntry(abs_path) else: self._recent_exe_menu.update(abs_path) def _updateRecentWorkingDir(self, path, remove=False): """ Updates the recently used menu with the current executable """ full_path = os.path.abspath(path) if self._recent_working_menu: if remove: self._recent_working_menu.removeEntry(full_path) else: self._recent_working_menu.update(full_path) def onPreferencesSaved(self): self._recent_args_menu.updateRecentlyOpened() self._recent_working_menu.updateRecentlyOpened() self._recent_exe_menu.updateRecentlyOpened() def addToMenu(self, menu): """ Adds menu entries specific to the Arguments to the menubar. """ workingMenu = menu.addMenu("Recent &working dirs") self._recent_working_menu = RecentlyUsedMenu(workingMenu, "execute/recentWorkingDirs", "execute/maxRecentWorkingDirs", 20, ) self._recent_working_menu.selected.connect(self.setWorkingDir) self._workingDirChanged() exeMenu = menu.addMenu("Recent &executables") self._recent_exe_menu = RecentlyUsedMenu(exeMenu, "execute/recentExes", "execute/maxRecentExes", 20, ) self._recent_exe_menu.selected.connect(self.setExecutablePath) argsMenu = menu.addMenu("Recent &arguments") self._recent_args_menu = RecentlyUsedMenu(argsMenu, "execute/recentArgs", "execute/maxRecentArgs", 20, ) self._recent_args_menu.selected.connect(self._setExecutableArgs) def clearRecentlyUsed(self): if self._recent_args_menu: self._recent_args_menu.clearValues() self._recent_working_menu.clearValues() self._recent_exe_menu.clearValues() self._workingDirChanged() def _mpiLineCursorChanged(self, old, new): self.mpi_checkbox.setChecked(True) def _threadsLineCursorChanged(self, old, new): self.threads_checkbox.setChecked(True)
class MainWindow(QMainWindow): OnlineHelpUrl = QUrl("https://eth-cscs.github.io/serialbox2/sdb.html") def __init__(self): super().__init__() Logger.info("Setup main window") self.__input_serializer_data = SerializerData("Input Serializer") self.__input_stencil_data = StencilData(self.__input_serializer_data) self.__reference_serializer_data = SerializerData( "Reference Serializer") self.__reference_stencil_data = StencilData( self.__reference_serializer_data) self.__stencil_field_mapper = StencilFieldMapper( self.__input_stencil_data, self.__reference_stencil_data, GlobalConfig()["async"]) self.__file_system_watcher = QFileSystemWatcher() self.__file_system_watcher.directoryChanged[str].connect( self.popup_reload_box) self.__file_system_watcher_last_modify = time() # Load from session? self.__session_manager = SessionManager() if GlobalConfig()["default_session"]: self.__session_manager.load_from_file() self.__session_manager.set_serializer_data( self.__input_serializer_data) self.__session_manager.set_serializer_data( self.__reference_serializer_data) # Setup GUI self.setWindowTitle('sdb - stencil debugger (%s)' % Version().sdb_version()) self.resize(1200, 600) if GlobalConfig()["center_window"]: self.center() if GlobalConfig()["move_window"]: self.move(GlobalConfig()["move_window"]) self.setWindowIcon(Icon("logo-small.png")) self.init_menu_tool_bar() # Tabs self.__tab_highest_valid_state = TabState.Setup self.__widget_tab = QTabWidget(self) # Setup tab self.__widget_tab.addTab( SetupWindow(self, self.__input_serializer_data, self.__reference_serializer_data), "Setup") # Stencil tab self.__widget_tab.addTab( StencilWindow(self, self.__stencil_field_mapper, self.__input_stencil_data, self.__reference_stencil_data), "Stencil") # Result tab self.__widget_tab.addTab( ResultWindow(self, self.__widget_tab.widget(TabState.Stencil.value), self.__stencil_field_mapper), "Result") # Error tab self.__widget_tab.addTab(ErrorWindow(self), "Error") self.__widget_tab.currentChanged.connect(self.switch_to_tab) self.__widget_tab.setTabEnabled(TabState.Setup.value, True) self.__widget_tab.setTabEnabled(TabState.Stencil.value, False) self.__widget_tab.setTabEnabled(TabState.Result.value, False) self.__widget_tab.setTabEnabled(TabState.Error.value, False) self.__widget_tab.setTabToolTip(TabState.Setup.value, "Setup Input and Refrence Serializer") self.__widget_tab.setTabToolTip( TabState.Stencil.value, "Set the stencil to compare and define the mapping of the fields") self.__widget_tab.setTabToolTip(TabState.Result.value, "View to comparison result") self.__widget_tab.setTabToolTip( TabState.Error.value, "Detailed error desscription of the current field") self.__tab_current_state = TabState.Setup self.set_tab_highest_valid_state(TabState.Setup) self.switch_to_tab(TabState.Setup) self.setCentralWidget(self.__widget_tab) # If the MainWindow is closed, kill all popup windows self.setAttribute(Qt.WA_DeleteOnClose) Logger.info("Starting main loop") self.show() def init_menu_tool_bar(self): Logger.info("Setup menu toolbar") action_exit = QAction("Exit", self) action_exit.setShortcut("Ctrl+Q") action_exit.setStatusTip("Exit the application") action_exit.triggered.connect(self.close) action_about = QAction("&About", self) action_about.setStatusTip("Show the application's About box") action_about.triggered.connect(self.popup_about_box) action_save_session = QAction(Icon("filesave.png"), "&Save", self) action_save_session.setStatusTip("Save current session") action_save_session.setShortcut("Ctrl+S") action_save_session.triggered.connect(self.save_session) action_open_session = QAction(Icon("fileopen.png"), "&Open", self) action_open_session.setShortcut("Ctrl+O") action_open_session.setStatusTip("Open session") action_open_session.triggered.connect(self.open_session) action_help = QAction(Icon("help.png"), "&Online Help", self) action_help.setStatusTip("Online Help") action_help.setToolTip("Online Help") action_help.triggered.connect(self.go_to_online_help) self.__action_continue = QAction(Icon("next_cursor.png"), "Continue", self) self.__action_continue.setStatusTip("Continue to next tab") self.__action_continue.triggered.connect(self.switch_to_next_tab) self.__action_continue.setEnabled(True) self.__action_back = QAction(Icon("prev_cursor.png"), "Back", self) self.__action_back.setStatusTip("Back to previous tab") self.__action_back.triggered.connect(self.switch_to_previous_tab) self.__action_back.setEnabled(False) self.__action_reload = QAction(Icon("step_in.png"), "Reload", self) self.__action_reload.setStatusTip( "Reload Input and Reference Serializer") self.__action_reload.setShortcut("Ctrl+R") self.__action_reload.triggered.connect(self.reload_serializer) self.__action_reload.setEnabled(False) self.__action_try_switch_to_error_tab = QAction( Icon("visualize.png"), "Detailed error description", self) self.__action_try_switch_to_error_tab.setStatusTip( "Detailed error desscription of the current field") self.__action_try_switch_to_error_tab.triggered.connect( self.try_switch_to_error_tab) self.__action_try_switch_to_error_tab.setEnabled(False) menubar = self.menuBar() menubar.setNativeMenuBar(False) self.statusBar() file_menu = menubar.addMenu('&File') file_menu.addAction(action_open_session) file_menu.addAction(action_save_session) file_menu.addAction(action_exit) edit_menu = menubar.addMenu('&Edit') edit_menu.addAction(self.__action_back) edit_menu.addAction(self.__action_continue) edit_menu.addAction(self.__action_reload) help_menu = menubar.addMenu('&Help') help_menu.addAction(action_about) help_menu.addAction(action_help) toolbar = self.addToolBar("Toolbar") toolbar.addAction(action_help) toolbar.addAction(action_open_session) toolbar.addAction(action_save_session) toolbar.addAction(self.__action_back) toolbar.addAction(self.__action_continue) toolbar.addAction(self.__action_reload) toolbar.addAction(self.__action_try_switch_to_error_tab) def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def closeEvent(self, event): self.__session_manager.update_serializer_data( self.__input_serializer_data) self.__session_manager.update_serializer_data( self.__reference_serializer_data) if GlobalConfig()["default_session"]: self.__session_manager.store_to_file() # ===----------------------------------------------------------------------------------------=== # TabWidgets # ==-----------------------------------------------------------------------------------------=== def tab_widget(self, idx): return self.__widget_tab.widget( idx if not isinstance(idx, TabState) else idx.value) def switch_to_tab(self, tab): idx = tab.value if isinstance(tab, TabState) else tab if self.__tab_current_state == TabState(idx): return Logger.info("Switching to %s tab" % TabState(idx).name) self.__tab_current_state = TabState(idx) self.__widget_tab.setCurrentIndex(idx) self.tab_widget(idx).make_update() self.__action_try_switch_to_error_tab.setEnabled( TabState(idx) == TabState.Result) # Error tab is always disabled if not in "Error" self.__widget_tab.setTabEnabled(TabState.Error.value, TabState(idx) == TabState.Error) # First tab if idx == 0: self.__action_continue.setEnabled(True) self.__action_back.setEnabled(False) # Last tab elif idx == self.__widget_tab.count() - 1: self.__action_continue.setEnabled(False) self.__action_back.setEnabled(True) # Middle tab else: self.__action_continue.setEnabled(True) self.__action_back.setEnabled(True) def set_tab_highest_valid_state(self, state): """Set the state at which the data is valid i.e everything <= self.valid_tab_state is valid """ self.__tab_highest_valid_state = state self.enable_tabs_according_to_tab_highest_valid_state() def enable_tabs_according_to_tab_highest_valid_state(self): """Enable/Disable tabs according to self.__tab_highest_valid_state """ if self.__tab_highest_valid_state == TabState.Setup: self.__widget_tab.setTabEnabled(TabState.Setup.value, True) self.__widget_tab.setTabEnabled(TabState.Stencil.value, False) self.__widget_tab.setTabEnabled(TabState.Result.value, False) self.__widget_tab.setTabEnabled(TabState.Error.value, False) self.__action_try_switch_to_error_tab.setEnabled(False) watched_directories = self.__file_system_watcher.directories() if watched_directories: self.__file_system_watcher.removePaths( self.__file_system_watcher.directories()) elif self.__tab_highest_valid_state == TabState.Stencil: self.__file_system_watcher.addPath( self.__input_serializer_data.serializer.directory) self.__file_system_watcher.addPath( self.__reference_stencil_data.serializer.directory) self.__widget_tab.setTabEnabled(TabState.Setup.value, True) self.__widget_tab.setTabEnabled(TabState.Stencil.value, True) self.__widget_tab.setTabEnabled(TabState.Result.value, False) self.__widget_tab.setTabEnabled(TabState.Error.value, False) self.__widget_tab.widget( TabState.Stencil.value).initial_field_match() self.__action_reload.setEnabled(True) self.__action_try_switch_to_error_tab.setEnabled(False) elif self.__tab_highest_valid_state == TabState.Result: self.__widget_tab.setTabEnabled(TabState.Setup.value, True) self.__widget_tab.setTabEnabled(TabState.Stencil.value, True) self.__widget_tab.setTabEnabled(TabState.Result.value, True) self.__widget_tab.setTabEnabled(TabState.Error.value, True) self.__action_try_switch_to_error_tab.setEnabled(True) elif self.__tab_highest_valid_state == TabState.Error: self.__widget_tab.setTabEnabled(TabState.Setup.value, True) self.__widget_tab.setTabEnabled(TabState.Stencil.value, True) self.__widget_tab.setTabEnabled(TabState.Result.value, True) self.__action_try_switch_to_error_tab.setEnabled(False) def switch_to_next_tab(self): self.__widget_tab.currentWidget().make_continue() def switch_to_previous_tab(self): self.__widget_tab.currentWidget().make_back() def try_switch_to_error_tab(self): if self.__widget_tab.widget( TabState.Result.value).try_switch_to_error_tab(): self.__widget_tab.setTabEnabled(TabState.Error.value, True) def error_window_set_result_data(self, result_data): self.__widget_tab.widget( TabState.Error.value).set_result_data(result_data) # ===----------------------------------------------------------------------------------------=== # PopupWidgets # ==-----------------------------------------------------------------------------------------=== def popup_about_box(self): self.__about_widget = PopupAboutWidget(self) def popup_error_box(self, msg): Logger.error( msg.replace("<b>", "").replace("</b>", "").replace("<br />", ":").replace("<br/>", ":")) msg_box = QMessageBox() msg_box.setWindowTitle("Error") msg_box.setIcon(QMessageBox.Critical) msg_box.setText(msg) msg_box.setStandardButtons(QMessageBox.Ok) reply = msg_box.exec_() # Blocking def popup_reload_box(self, path): self.__file_system_watcher.blockSignals(True) reply = QMessageBox.question( self, "Reload serializer?", "The path \"%s\" has changed.\nDo want to reload the serializers?" % path, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.Yes: self.reload_serializer() self.__file_system_watcher.blockSignals(False) # ===----------------------------------------------------------------------------------------=== # Session manager # ==-----------------------------------------------------------------------------------------=== def save_session(self): Logger.info("Try saving current session") dialog = QFileDialog(self, "Save current session") dialog.setAcceptMode(QFileDialog.AcceptSave) dialog.setDefaultSuffix("json") dialog.setDirectory(getcwd()) if not dialog.exec_(): Logger.info("Abort saving current session") return filename = dialog.selectedFiles() self.__session_manager.update_serializer_data( self.__input_serializer_data) self.__session_manager.update_serializer_data( self.__reference_serializer_data) ret, msglist = self.__session_manager.store_to_file(filename[0]) if not ret: self.popup_error_box("Failed to save configuration file: %s\n%s " % (filename[0], msglist[0])) def open_session(self): Logger.info("Try opening session") filename = QFileDialog.getOpenFileName( self, "Open Session", getcwd(), "JSON configuration (*.json)")[0] if filename is None or filename is "": Logger.info("Abort opening session") return ret, msglist = self.__session_manager.load_from_file(filename) if not ret: self.popup_error_box("Failed to load configuration file: %s\n%s " % (filename, msglist[0])) else: Logger.info("Successfully opened session") self.__session_manager.set_serializer_data( self.__input_serializer_data) self.__session_manager.set_serializer_data( self.__reference_serializer_data) self.switch_to_tab(TabState.Setup) @property def session_manager(self): return self.__session_manager # ===----------------------------------------------------------------------------------------=== # Reload Serializer # ==-----------------------------------------------------------------------------------------=== def reload_serializer(self): Logger.info("Reloading serializers") try: self.__input_serializer_data.reload() self.__reference_serializer_data.reload() if self.__widget_tab.currentIndex() == TabState.Error.value: self.switch_to_tab(TabState.Result) self.__widget_tab.currentWidget().make_update() except RuntimeError as e: self.popup_error_box(str(e)) self.set_tab_highest_valid_state(TabState.Setup) self.switch_to_tab(TabState.Setup) self.__widget_tab.currentWidget().make_update() # ===----------------------------------------------------------------------------------------=== # Online help # ==-----------------------------------------------------------------------------------------=== def go_to_online_help(self): Logger.info("Opening online help") QDesktopServices.openUrl(MainWindow.OnlineHelpUrl)
class QmyMainWindow(QMainWindow): def __init__(self, parent=None): super().__init__(parent) #调用父类构造函数,创建窗体 self.ui = Ui_MainWindow() #创建UI对象 self.ui.setupUi(self) #构造UI界面 self.ui.toolBox.setCurrentIndex(0) self.fileWatcher = QFileSystemWatcher() self.fileWatcher.directoryChanged.connect(self.do_directoryChanged) self.fileWatcher.fileChanged.connect(self.do_fileChanged) ## ==============自定义功能函数======================== def __showBtnInfo(self, btn): ##显示按钮的text()和toolTip() self.ui.textEdit.appendPlainText("====" + btn.text()) self.ui.textEdit.appendPlainText(btn.toolTip() + "\n") ## ==============event处理函数========================== ## ==========由connectSlotsByName()自动连接的槽函数============ @pyqtSlot() ##"选择文件"按钮 def on_btnOpenFile_clicked(self): curDir = QDir.currentPath() #获取当前路径 aFile, filt = QFileDialog.getOpenFileName(self, "打开文件", curDir, "所有文件(*.*)") self.ui.editFile.setText(aFile) @pyqtSlot() ##"选择目录"按钮 def on_btnOpenDir_clicked(self): curDir = QDir.currentPath() aDir = QFileDialog.getExistingDirectory(self, "选择一个目录", curDir, QFileDialog.ShowDirsOnly) self.ui.editDir.setText(aDir) @pyqtSlot() ##"清空"按钮 def on_btnClear_clicked(self): self.ui.textEdit.clear() ## =========QFile类 的静态函数=========== @pyqtSlot() ##类函数copy() def on_btnFile_copy_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text().strip() #源文件 if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return fileInfo = QFileInfo(sous) newFile = fileInfo.path() + "/" + fileInfo.baseName( ) + "--副本." + fileInfo.suffix() if QFile.copy(sous, newFile): self.ui.textEdit.appendPlainText("源文件:" + sous) self.ui.textEdit.appendPlainText("复制为文件:" + newFile + "\n") else: self.ui.textEdit.appendPlainText("复制文件失败") @pyqtSlot() ##类函数exists() def on_btnFile_exists_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text().strip() #源文件 if QFile.exists(sous): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") @pyqtSlot() ##类函数remove() def on_btnFile_remove_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text().strip() #源文件 if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return ret = QMessageBox.question(self, "确认删除", "确定要删除这个文件吗\n\n" + sous) if (ret != QMessageBox.Yes): return if QFile.remove(sous): self.ui.textEdit.appendPlainText("成功删除文件:" + sous + "\n") else: self.ui.textEdit.appendPlainText("删除文件失败:" + sous + "\n") @pyqtSlot() ##类函数rename() def on_btnFile_rename_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text().strip() #源文件 if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return fileInfo = QFileInfo(sous) newFile = fileInfo.path() + "/" + fileInfo.baseName( ) + ".XZY" #更改文件后缀为".XYZ" if QFile.rename(sous, newFile): self.ui.textEdit.appendPlainText("源文件:" + sous) self.ui.textEdit.appendPlainText("重命名为:" + newFile + "\n") else: self.ui.textEdit.appendPlainText("重命名文件失败\n") ## =========QFileInfo类=========== @pyqtSlot() ##absoluteFilePath() def on_btnInfo_absFilePath_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.absoluteFilePath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##absolutePath() def on_btnInfo_absPath_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.absolutePath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##fileName() def on_btnInfo_fileName_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.fileName() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##filePath() def on_btnInfo_filePath_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.filePath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##size() def on_btnInfo_size_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) btCount = fileInfo.size() #字节数 text = "%d Bytes" % btCount self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##path() def on_btnInfo_path_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.path() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##baseName() def on_btnInfo_baseName_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.baseName() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##completeBaseName() def on_btnInfo_baseName2_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.completeBaseName() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##suffix() def on_btnInfo_suffix_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.suffix() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##completeSuffix() def on_btnInfo_suffix2_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) text = fileInfo.completeSuffix() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##isDir() def on_btnInfo_isDir_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editDir.text()) if fileInfo.isDir(): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") @pyqtSlot() ##isFile() def on_btnInfo_isFile_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) if fileInfo.isFile(): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") @pyqtSlot() ##isExecutable() def on_btnInfo_isExec_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) if fileInfo.isExecutable(): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") @pyqtSlot() ##birthTime() ,替代了过时的created()函数 def on_btnInfo_birthTime_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) dt = fileInfo.birthTime() # QDateTime text = dt.toString("yyyy-MM-dd hh:mm:ss") self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##lastModified() def on_btnInfo_lastModified_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) dt = fileInfo.lastModified() # QDateTime text = dt.toString("yyyy-MM-dd hh:mm:ss") self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##lastRead() def on_btnInfo_lastRead_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) dt = fileInfo.lastRead() # QDateTime text = dt.toString("yyyy-MM-dd hh:mm:ss") self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##类函数exists() def on_btnInfo_exists_clicked(self): self.__showBtnInfo(self.sender()) if QFileInfo.exists(self.ui.editFile.text()): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") @pyqtSlot() ##接口函数exists() def on_btnInfo_exists2_clicked(self): self.__showBtnInfo(self.sender()) fileInfo = QFileInfo(self.ui.editFile.text()) if fileInfo.exists(): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") ## ==================QDir类======================== @pyqtSlot() ##tempPath() def on_btnDir_tempPath_clicked(self): self.__showBtnInfo(self.sender()) text = QDir.tempPath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##rootPath() def on_btnDir_rootPath_clicked(self): self.__showBtnInfo(self.sender()) text = QDir.rootPath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##homePath() def on_btnDir_homePath_clicked(self): self.__showBtnInfo(self.sender()) text = QDir.homePath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##drives() def on_btnDir_drives_clicked(self): self.__showBtnInfo(self.sender()) strList = QDir.drives() #QFileInfoList for line in strList: #line 是QFileInfo类型 self.ui.textEdit.appendPlainText(line.path()) self.ui.textEdit.appendPlainText("") @pyqtSlot() ##currentPath() def on_btnDir_curPath_clicked(self): self.__showBtnInfo(self.sender()) text = QDir.currentPath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##setCurrent() def on_btnDir_setCurPath_clicked(self): self.__showBtnInfo(self.sender()) curDir = QDir.currentPath() text = QFileDialog.getExistingDirectory(self, "选择一个目录", curDir, QFileDialog.ShowDirsOnly) QDir.setCurrent(text) self.ui.textEdit.appendPlainText("选择了一个目录作为当前目录:\n" + text + "\n") @pyqtSlot() ##mkdir() def on_btnDir_mkdir_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text().strip() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return subDir = "subdir1" dirObj = QDir(sous) if dirObj.mkdir(subDir): self.ui.textEdit.appendPlainText("新建一个子目录: " + subDir + "\n") else: self.ui.textEdit.appendPlainText("创建目录失败\n") @pyqtSlot() ##rmdir() def on_btnDir_rmdir_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text().strip() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return dirObj = QDir(sous) if dirObj.rmdir(sous): self.ui.textEdit.appendPlainText("成功删除所选目录\n" + sous + "\n") else: self.ui.textEdit.appendPlainText("删除目录失败,目录下必须为空\n") @pyqtSlot() ##remove() def on_btnDir_remove_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text().strip() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return parDir = self.ui.editDir.text().strip() if parDir == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return dirObj = QDir(parDir) if dirObj.remove(sous): self.ui.textEdit.appendPlainText("成功删除文件:\n" + sous + "\n") else: self.ui.textEdit.appendPlainText("删除文件失败\n") @pyqtSlot() ##rename() def on_btnDir_rename_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return parDir = self.ui.editDir.text().strip() if parDir == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return dirObj = QDir(parDir) fileInfo = QFileInfo(sous) newFile = fileInfo.path() + "/" + fileInfo.baseName() + ".XYZ" if dirObj.rename(sous, newFile): self.ui.textEdit.appendPlainText("源文件:" + sous) self.ui.textEdit.appendPlainText("重命名为:" + newFile + "\n") else: self.ui.textEdit.appendPlainText("重命名文件失败\n") @pyqtSlot() ##setPath(),改换QDir所指的目录 def on_btnDir_setPath_clicked(self): self.__showBtnInfo(self.sender()) curDir = QDir.currentPath() lastDir = QDir(curDir) self.ui.textEdit.appendPlainText("选择目录之前,QDir所指目录是:" + lastDir.absolutePath()) aDir = QFileDialog.getExistingDirectory(self, "选择一个目录", curDir, QFileDialog.ShowDirsOnly) if aDir == "": return lastDir.setPath(aDir) self.ui.textEdit.appendPlainText("\n选择目录之后,QDir所指目录是:" + lastDir.absolutePath() + "\n") @pyqtSlot() ##removeRecursively() def on_btnDir_removeALL_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text().strip() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return dirObj = QDir(sous) ret = QMessageBox.question(self, "确认删除", "确认删除目录下的所有文件及目录吗?\n" + sous) if ret != QMessageBox.Yes: return if dirObj.removeRecursively(): self.ui.textEdit.appendPlainText("删除目录及文件成功\n") else: self.ui.textEdit.appendPlainText("删除目录及文件失败\n") @pyqtSlot() ##absoluteFilePath() def on_btnDir_absFilePath_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return parDir = QDir.currentPath() dirObj = QDir(parDir) text = dirObj.absoluteFilePath(sous) self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##absolutePath() def on_btnDir_absPath_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return dirObj = QDir(sous) text = dirObj.absolutePath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##canonicalPath() def on_btnDir_canonPath_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个目录") return dirObj = QDir(sous) text = dirObj.canonicalPath() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##filePath() def on_btnDir_filePath_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return parDir = QDir.currentPath() dirObj = QDir(parDir) text = dirObj.filePath(sous) self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##exists() def on_btnDir_exists_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text() ## if sous=="": ## self.ui.textEdit.appendPlainText("请先选择一个目录") ## return dirObj = QDir(sous) #若sous为空,则使用其当前目录 self.ui.textEdit.appendPlainText(dirObj.absolutePath() + "\n") if dirObj.exists(): self.ui.textEdit.appendPlainText("True \n") else: self.ui.textEdit.appendPlainText("False \n") @pyqtSlot() ##dirName() def on_btnDir_dirName_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text() ## if sous=="": ## self.ui.textEdit.appendPlainText("请先选择一个目录") ## return dirObj = QDir(sous) #若sous为空,则使用其当前目录 text = dirObj.dirName() self.ui.textEdit.appendPlainText(text + "\n") @pyqtSlot() ##entryList()dirs def on_btnDir_listDir_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text() dirObj = QDir(sous) #若sous为空,则使用其当前目录 strList = dirObj.entryList(QDir.Dirs | QDir.NoDotAndDotDot) self.ui.textEdit.appendPlainText("所选目录下的所有目录:") for line in strList: self.ui.textEdit.appendPlainText(line) self.ui.textEdit.appendPlainText("\n") @pyqtSlot() ##entryList()files def on_btnDir_listFile_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editDir.text() dirObj = QDir(sous) #若sous为空,则使用其当前目录 strList = dirObj.entryList(QDir.Files) self.ui.textEdit.appendPlainText("所选目录下的所有文件:") for line in strList: self.ui.textEdit.appendPlainText(line) self.ui.textEdit.appendPlainText("\n") ## ==========QFileSystemWatcher类=================== @pyqtSlot() ##addPath()添加监听目录 def on_btnWatch_addDir_clicked(self): self.__showBtnInfo(self.sender()) curDir = QDir.currentPath() aDir = QFileDialog.getExistingDirectory(self, "选择一个需要监听的目录", curDir, QFileDialog.ShowDirsOnly) self.fileWatcher.addPath(aDir) #添加监听目录 self.ui.textEdit.appendPlainText("添加的监听目录:") self.ui.textEdit.appendPlainText(aDir + "\n") @pyqtSlot() ##addPaths()添加监听文件 def on_btnWatch_addFiles_clicked(self): self.__showBtnInfo(self.sender()) curDir = QDir.currentPath() fileList, flt = QFileDialog.getOpenFileNames(self, "选择需要监听的文件", curDir, "所有文件 (*.*)") self.fileWatcher.addPaths(fileList) #添加监听文件列表 self.ui.textEdit.appendPlainText("添加的监听文件:") for lineStr in fileList: self.ui.textEdit.appendPlainText(lineStr) self.ui.textEdit.appendPlainText("") @pyqtSlot() ##removePaths()移除所有监听的文件和目录 def on_btnWatch_remove_clicked(self): self.__showBtnInfo(self.sender()) self.ui.textEdit.appendPlainText("移除所有监听的目录和文件\n") dirList = self.fileWatcher.directories() self.fileWatcher.removePaths(dirList) fileList = self.fileWatcher.files() self.fileWatcher.removePaths(fileList) @pyqtSlot() ##显示监听目录,directories() def on_btnWatch_dirs_clicked(self): self.__showBtnInfo(self.sender()) strList = self.fileWatcher.directories() self.ui.textEdit.appendPlainText("正在监听的目录:") for line in strList: self.ui.textEdit.appendPlainText(line) self.ui.textEdit.appendPlainText("\n") @pyqtSlot() ##显示监听文件,files() def on_btnWatch_files_clicked(self): self.__showBtnInfo(self.sender()) strList = self.fileWatcher.files() self.ui.textEdit.appendPlainText("正在监听的文件:") for line in strList: self.ui.textEdit.appendPlainText(line) self.ui.textEdit.appendPlainText("\n") ## =============自定义槽函数=============================== def do_directoryChanged(self, path): ##目录发生变化 self.ui.textEdit.appendPlainText(path) self.ui.textEdit.appendPlainText("目录发生了变化\n") def do_fileChanged(self, path): ##文件发生变化 self.ui.textEdit.appendPlainText(path) self.ui.textEdit.appendPlainText("文件发生了变化\n")
class ExternalEditor(QObject): """Class to simplify editing a text in an external editor. Attributes: _text: The current text before the editor is opened. _filename: The name of the file to be edited. _remove_file: Whether the file should be removed when the editor is closed. _proc: The GUIProcess of the editor. _watcher: A QFileSystemWatcher to watch the edited file for changes. Only set if watch=True. _content: The last-saved text of the editor. Signals: file_updated: The text in the edited file was updated. arg: The new text. editing_finished: The editor process was closed. """ file_updated = pyqtSignal(str) editing_finished = pyqtSignal() def __init__(self, parent=None, watch=False): super().__init__(parent) self._filename = None self._proc = None self._remove_file = None self._watcher = QFileSystemWatcher(parent=self) if watch else None self._content = None def _cleanup(self, *, successful): """Clean up temporary files after the editor closed. Args: successful: Whether the editor exited successfully, i.e. the file can be deleted. """ assert self._remove_file is not None if (self._watcher is not None and not sip.isdeleted(self._watcher) and self._watcher.files()): failed = self._watcher.removePaths(self._watcher.files()) if failed: log.procs.error("Failed to unwatch paths: {}".format(failed)) if self._filename is None or not self._remove_file: # Could not create initial file. return assert self._proc is not None if successful: try: os.remove(self._filename) except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to delete tempfile... ({})".format(e)) else: message.info( f"Keeping file {self._filename} as the editor process exited " "abnormally") @pyqtSlot(int, QProcess.ExitStatus) def _on_proc_closed(self, _exitcode, exitstatus): """Write the editor text into the form field and clean up tempfile. Callback for QProcess when the editor was closed. """ if sip.isdeleted(self): # pragma: no cover log.procs.debug("Ignoring _on_proc_closed for deleted editor") return log.procs.debug("Editor closed") if exitstatus != QProcess.NormalExit: # No error/cleanup here, since we already handle this in # on_proc_error. return # do a final read to make sure we don't miss the last signal assert self._proc is not None self._on_file_changed(self._filename) self.editing_finished.emit() self._cleanup(successful=self._proc.outcome.was_successful()) @pyqtSlot(QProcess.ProcessError) def _on_proc_error(self, _err): self._cleanup(successful=False) def edit(self, text, caret_position=None): """Edit a given text. Args: text: The initial text to edit. caret_position: The position of the caret in the text. """ if self._filename is not None: raise ValueError("Already editing a file!") try: self._filename = self._create_tempfile(text, 'qutebrowser-editor-') except (OSError, UnicodeEncodeError) as e: message.error("Failed to create initial file: {}".format(e)) return self._remove_file = True line, column = self._calc_line_and_column(text, caret_position) self._start_editor(line=line, column=column) def backup(self): """Create a backup if the content has changed from the original.""" if not self._content: return try: fname = self._create_tempfile(self._content, 'qutebrowser-editor-backup-') message.info('Editor backup at {}'.format(fname)) except OSError as e: message.error('Failed to create editor backup: {}'.format(e)) def _create_tempfile(self, text, prefix): # Close while the external process is running, as otherwise systems # with exclusive write access (e.g. Windows) may fail to update # the file from the external editor, see # https://github.com/qutebrowser/qutebrowser/issues/1767 with tempfile.NamedTemporaryFile(mode='w', prefix=prefix, encoding=config.val.editor.encoding, delete=False) as fobj: if text: fobj.write(text) return fobj.name @pyqtSlot(str) def _on_file_changed(self, path): try: with open(path, 'r', encoding=config.val.editor.encoding) as f: text = f.read() except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to read back edited file: {}".format(e)) return log.procs.debug("Read back: {}".format(text)) if self._content != text: self._content = text self.file_updated.emit(text) def edit_file(self, filename): """Edit the file with the given filename.""" if not os.path.exists(filename): with open(filename, 'w', encoding='utf-8'): pass self._filename = filename self._remove_file = False self._start_editor() def _start_editor(self, line=1, column=1): """Start the editor with the file opened as self._filename. Args: line: the line number to pass to the editor column: the column number to pass to the editor """ self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc.finished.connect(self._on_proc_closed) self._proc.error.connect(self._on_proc_error) editor = config.val.editor.command executable = editor[0] if self._watcher: assert self._filename is not None ok = self._watcher.addPath(self._filename) if not ok: log.procs.error("Failed to watch path: {}".format( self._filename)) self._watcher.fileChanged.connect( # type: ignore[attr-defined] self._on_file_changed) args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args) def _calc_line_and_column(self, text, caret_position): r"""Calculate line and column numbers given a text and caret position. Both line and column are 1-based indexes, because that's what most editors use as line and column starting index. By "most" we mean at least vim, nvim, gvim, emacs, atom, sublimetext, notepad++, brackets, visual studio, QtCreator and so on. To find the line we just count how many newlines there are before the caret and add 1. To find the column we calculate the difference between the caret and the last newline before the caret. For example in the text `aaa\nbb|bbb` (| represents the caret): caret_position = 6 text[:caret_position] = `aaa\nbb` text[:caret_position].count('\n') = 1 caret_position - text[:caret_position].rfind('\n') = 3 Thus line, column = 2, 3, and the caret is indeed in the second line, third column Args: text: the text for which the numbers must be calculated caret_position: the position of the caret in the text, or None Return: A (line, column) tuple of (int, int) """ if caret_position is None: return 1, 1 line = text[:caret_position].count('\n') + 1 column = caret_position - text[:caret_position].rfind('\n') return line, column def _sub_placeholder(self, arg, line, column): """Substitute a single placeholder. If the `arg` input to this function is a valid placeholder it will be substituted with the appropriate value, otherwise it will be left unchanged. Args: arg: an argument of editor.command. line: the previously-calculated line number for the text caret. column: the previously-calculated column number for the text caret. Return: The substituted placeholder or the original argument. """ replacements = { '{}': self._filename, '{file}': self._filename, '{line}': str(line), '{line0}': str(line - 1), '{column}': str(column), '{column0}': str(column - 1) } for old, new in replacements.items(): arg = arg.replace(old, new) return arg
class Editor(CodeEditor, ComponentMixin): name = 'Code Editor' # This signal is emitted whenever the currently-open file changes and # autoreload is enabled. triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) preferences = Parameter.create(name='Preferences', children=[{ 'name': 'Font size', 'type': 'int', 'value': 12 }, { 'name': 'Autoreload', 'type': 'bool', 'value': False }, { 'name': 'Autoreload delay', 'type': 'int', 'value': 50 }, { 'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False }, { 'name': 'Line wrap', 'type': 'bool', 'value': False }, { 'name': 'Color scheme', 'type': 'list', 'values': ['Spyder', 'Monokai', 'Zenburn'], 'value': 'Spyder' }]) EXTENSIONS = 'py' def __init__(self, parent=None): self._watched_file = None super(Editor, self).__init__(parent) ComponentMixin.__init__(self) self.setup_editor(linenumbers=True, markers=True, edge_line=False, tab_mode=False, show_blanks=True, font=QFontDatabase.systemFont( QFontDatabase.FixedFont), language='Python', filename='') self._actions = \ {'File' : [QAction(icon('new'), 'New', self, shortcut='ctrl+N', triggered=self.new), QAction(icon('open'), 'Open', self, shortcut='ctrl+O', triggered=self.open), QAction(icon('save'), 'Save', self, shortcut='ctrl+S', triggered=self.save), QAction(icon('save_as'), 'Save as', self, shortcut='ctrl+shift+S', triggered=self.save_as), QAction(icon('autoreload'), 'Automatic reload and preview', self,triggered=self.autoreload, checkable=True, checked=False, objectName='autoreload'), ]} for a in self._actions.values(): self.addActions(a) self._fixContextMenu() # autoreload support self._file_watcher = QFileSystemWatcher(self) # we wait for 50ms after a file change for the file to be written completely self._file_watch_timer = QTimer(self) self._file_watch_timer.setInterval( self.preferences['Autoreload delay']) self._file_watch_timer.setSingleShot(True) self._file_watcher.fileChanged.connect( lambda val: self._file_watch_timer.start()) self._file_watch_timer.timeout.connect(self._file_changed) self.updatePreferences() def _fixContextMenu(self): menu = self.menu menu.removeAction(self.run_cell_action) menu.removeAction(self.run_cell_and_advance_action) menu.removeAction(self.run_selection_action) menu.removeAction(self.re_run_last_cell_action) def updatePreferences(self, *args): self.set_color_scheme(self.preferences['Color scheme']) font = self.font() font.setPointSize(self.preferences['Font size']) self.set_font(font) self.findChild(QAction, 'autoreload') \ .setChecked(self.preferences['Autoreload']) self._file_watch_timer.setInterval( self.preferences['Autoreload delay']) self.toggle_wrap_mode(self.preferences['Line wrap']) self._clear_watched_paths() self._watch_paths() def confirm_discard(self): if self.modified: rv = confirm( self, 'Please confirm', 'Current document is not saved - do you want to continue?') else: rv = True return rv def new(self): if not self.confirm_discard(): return self.set_text('') self.filename = '' self.reset_modified() def open(self): if not self.confirm_discard(): return curr_dir = Path(self.filename).abspath().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) if fname != '': self.load_from_file(fname) def load_from_file(self, fname): self.set_text_from_file(fname) self.filename = fname self.reset_modified() def save(self): if self._filename != '': if self.preferences['Autoreload']: self._file_watcher.blockSignals(True) self._file_watch_timer.stop() with open(self._filename, 'w') as f: f.write(self.toPlainText()) if self.preferences['Autoreload']: self._file_watcher.blockSignals(False) self.triggerRerender.emit(True) self.reset_modified() else: self.save_as() def save_as(self): fname = get_save_filename(self.EXTENSIONS) if fname != '': with open(fname, 'w') as f: f.write(self.toPlainText()) self.filename = fname self.reset_modified() def _update_filewatcher(self): if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): self._clear_watched_paths() self._watched_file = None if self.preferences[ 'Autoreload'] and self.filename and self.filename != self._watched_file: self._watched_file = self._filename self._watch_paths() @property def filename(self): return self._filename @filename.setter def filename(self, fname): self._filename = fname self._update_filewatcher() self.sigFilenameChanged.emit(fname) def _clear_watched_paths(self): paths = self._file_watcher.files() if paths: self._file_watcher.removePaths(paths) def _watch_paths(self): if Path(self._filename).exists(): self._file_watcher.addPath(self._filename) if self.preferences['Autoreload: watch imported modules']: module_paths = self.get_imported_module_paths(self._filename) if module_paths: self._file_watcher.addPaths(module_paths) # callback triggered by QFileSystemWatcher def _file_changed(self): # neovim writes a file by removing it first so must re-add each time self._watch_paths() self.set_text_from_file(self._filename) self.triggerRerender.emit(True) # Turn autoreload on/off. def autoreload(self, enabled): self.preferences['Autoreload'] = enabled self._update_filewatcher() def reset_modified(self): self.document().setModified(False) @property def modified(self): return self.document().isModified() def saveComponentState(self, store): if self.filename != '': store.setValue(self.name + '/state', self.filename) def restoreComponentState(self, store): filename = store.value(self.name + '/state') if filename and self.filename == '': try: self.load_from_file(filename) except IOError: self._logger.warning(f'could not open {filename}') def get_imported_module_paths(self, module_path): finder = ModuleFinder([os.path.dirname(module_path)]) imported_modules = [] try: finder.run_script(module_path) except SyntaxError as err: self._logger.warning(f'Syntax error in {module_path}: {err}') except Exception as err: self._logger.warning( f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}' ) else: for module_name, module in finder.modules.items(): if module_name != '__main__': path = getattr(module, '__file__', None) if path is not None and os.path.isfile(path): imported_modules.append(path) return imported_modules