class MainWatcher: def __init__(self): self.watcher = QFileSystemWatcher() self.folders = [] self.files = [] def connect(self): self.watcher.directoryChanged.connect(self.directory_changed) def add_paths(self, list_of_paths): self.watcher.addPaths(list_of_paths) def directory_changed(self, path): for f in listdir(path): complete_path = join(path, f) sub_folder = self.map_path_to_folder_id(path) if not exists(complete_path): continue if complete_path in self.folders or complete_path in self.files: continue if isfile(complete_path): fw = FileWatcher() fw.subfolder = sub_folder fw.base_path = complete_path fw.connect() fw.finished.connect(self.copied_file) self.files.append(fw) else: fw = SubFolderWatcher() fw.base_path = complete_path fw.subfolder = sub_folder fw.connect() fw.finished.connect(self.copied_folder) self.folders.append(fw) def copied_file(self, path): try: i = self.files.index(path) except IndexError: pass else: del self.files[i] def copied_folder(self, path): try: i = self.folders.index(path) except IndexError: pass else: del self.folders[i] @staticmethod def map_path_to_folder_id(path): normalized_path = normpath(path) ending = split(normalized_path)[-1] return ending
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 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 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 SubFolderWatcher(QObject): finished = pyqtSignal(str, int) def __init__(self): super(SubFolderWatcher, self).__init__() self.base_path = '' self.subfolder = '' self.files = {} self.finished_checking = False self.watcher = QFileSystemWatcher() self.timer = QTimer() self.check_threshold = 3 self.check_number = 0 self.sub_folder = '' def connect(self): self.watcher.addPaths([self.base_path]) self.watcher.directoryChanged.connect( lambda y: self.directory_changed(y)) self.watcher.fileChanged.connect(lambda x: self.file_changed(x)) self.timer.timeout.connect(self.check_files) self.timer.start(timer_delay * 1000) def check_files(self): finished = True if exists(self.base_path): for sub_path in Path(self.base_path).rglob('*'): new_st_mtime = int(stat(sub_path).st_mtime) path_name = normpath(sub_path) if path_name not in self.files: self.files[path_name] = new_st_mtime if self.files[path_name] != new_st_mtime: finished = False self.files[path_name] = new_st_mtime if finished: self.check_number += 1 if self.check_number > self.check_threshold: self.timer.stop() self.watcher.removePath(self.base_path) self.copy_folder() else: self.finished_checking = True def copy_file(self): pass def copy_folder(self): try: shutil.move(self.base_path, join(home, self.subfolder)) except shutil.Error: remove(self.base_path) else: pass """ NOTE: On my home system, I sometimes use the following code to change groups and owner permissions of the copied files. I would personally not use this in a production environment, especially with the 775 permissions. The following code doesn't make sense to run on Windows, so you can ignore if you're on that system. local = join(home, self.subfolder) chmod(local, 0o775) shutil.chown(local, 'grp', 'usr') for root, dirs, files in walk(local): chmod(root, 0o775) shutil.chown(root, 'grp', 'usr') for file_name in files: chmod(join(root, file_name), 0o775) shutil.chown(join(root, file_name), 'grp', 'usr') """ self.finished_checking = True self.finished.emit(self.base_path) @pyqtSlot(str) def directory_changed(self, path): if self.timer.isActive(): self.timer.stop() self.timer.start(timer_delay * 1000) for sub_path in Path(path).rglob('*'): if isfile(sub_path): path_name = normpath(join(path, sub_path.name)) self.files[path_name] = int(stat(self.base_path).st_mtime) def file_changed(self): pass def __eq__(self, other): try: return other.base_path == self.base_path except AttributeError: return other == self.base_path
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
class Window(QMainWindow, Ui_MainWindow): def __init__(self): QMainWindow.__init__(self) self.setupUi(self) self.pbConnect.setProperty("css", True) self.pageHost.setMain(self) self.pageHost.connectPressed.connect(self.onConnectHost) self.pageHost.cancelPressed.connect(self.showPage) self.pageLogin.setMain(self) self.pageLogin.connectPressed.connect(self.onConnectLogin) self.pageLogin.cancelPressed.connect(self.showPage) self.pageAbout.setMain(self) self.pageAbout.okPressed.connect(self.showPage) self.loaded = False self.setWindowTitle('Gold Drive') app_icon = QIcon() app_icon.addFile(':/images/icon_16.png', QSize(16, 16)) app_icon.addFile(':/images/icon_32.png', QSize(32, 32)) app_icon.addFile(':/images/icon_64.png', QSize(64, 64)) app_icon.addFile(':/images/icon_128.png', QSize(128, 128)) QApplication.setWindowIcon(app_icon) stream = QFile(':/style/style.css') if stream.open(QIODevice.ReadOnly | QFile.Text): self.setStyleSheet(QTextStream(stream).readAll()) # initial values from settings self.settings = QSettings("sganis", "golddrive") if self.settings.value("geometry"): self.restoreGeometry(self.settings.value("geometry")) self.restoreState(self.settings.value("windowState")) self.progressBar.setVisible(False) self.lblMessage.setText("") # self.widgetPassword.setVisible(False) self.returncode = util.ReturnCode.NONE # read config.json self.configfile = fr'{util.DIR}\..\config.json' self.param = {} self.config = util.load_config(self.configfile) # path = os.environ['PATH'] # sshfs_path = self.config.get('sshfs_path','') # sanfs_path = self.config.get('sanfs_path','') # os.environ['PATH'] = fr'{sanfs_path};{sshfs_path};{path}' self.updateCombo(self.settings.value("cboParam", 0)) self.fillParam() self.lblUserHostPort.setText(self.param['userhostport']) menu = Menu(self.pbHamburger) menu.addAction('&Connect', self.mnuConnect) menu.addAction('&Disconnect', self.mnuDisconnect) menu.addAction('Disconnect &all drives', self.mnuDisconnectAll) menu.addAction('&Open program location', self.mnuOpenProgramLocation) menu.addAction('Open &terminal', self.mnuOpenTerminal) menu.addAction('Open &log file', self.mnuOpenLogFile) menu.addAction('&Restart Explorer.exe', self.mnuRestartExplorer) menu.addAction('&About...', self.mnuAbout) self.pbHamburger.setMenu(menu) # worker for commands self.worker = Worker() self.worker.workDone.connect(self.onWorkDone) # worker for update self.updater = Updater(self.config["update_url"]) self.updater.checkDone.connect(self.onCheckUpdateDone) self.updater.doCheck() # worker for watching config.json changes self.watcher = QFileSystemWatcher() self.watcher.addPaths([self.configfile]) self.watcher.fileChanged.connect(self.onConfigFileChanged) self.checkDriveStatus() self.showPage(util.Page.MAIN) self.lblMessage.linkActivated.connect(self.on_lblMessage_linkActivated) self.loaded = True def start(self, message): self.showPage(util.Page.MAIN) self.returncode = util.ReturnCode.NONE self.pbConnect.setEnabled(False) self.progressBar.setVisible(True) self.showMessage(message) def end(self, message=''): self.showPage(util.Page.MAIN) self.progressBar.setVisible(False) self.pbConnect.setEnabled(True) self.showMessage(message) def showMessage(self, message): is_error = (self.returncode != util.ReturnCode.OK and self.returncode != util.ReturnCode.NONE) self.lblMessage.setText(message) self.lblMessage.setProperty("error", is_error) self.lblMessage.style().unpolish(self.lblMessage) self.lblMessage.style().polish(self.lblMessage) def setPbConnectText(self, status): self.pbConnect.setText('&CONNECT') if status == 'CONNECTED': self.pbConnect.setText('&DISCONNECT') elif status == 'BROKEN': self.pbConnect.setText('&REPAIR') elif status == 'IN USE': self.pbConnect.setEnabled(False) @pyqtSlot(str, util.ReturnBox) def onWorkDone(self, task, rb): self.returncode = rb.returncode if task == 'check_drive': if rb.drive_status == 'CONNECTED': drive = self.param['drive'] link = util.make_hyperlink('open_drive', 'Open') msg = util.rich_text(f"{rb.drive_status}\n\n{link}") else: msg = rb.drive_status if rb.error: msg = rb.error self.end(msg) elif task == 'connect': if rb.returncode == util.ReturnCode.BAD_DRIVE: self.end(rb.error) elif rb.returncode == util.ReturnCode.BAD_HOST: self.showPage(util.Page.HOST) self.pageHost.showError(rb.error) self.progressBar.setVisible(False) elif rb.returncode == util.ReturnCode.BAD_LOGIN: self.showPage(util.Page.LOGIN) self.pageLogin.showError(rb.error) self.progressBar.setVisible(False) elif rb.returncode == util.ReturnCode.BAD_MOUNT: self.end(rb.error) elif rb.returncode == util.ReturnCode.BAD_WINFSP: msg = ( f"WinFSP is not installed\n\n{ util.make_hyperlink('install_winfsp', 'Install') }" ) self.end(util.rich_text(msg)) else: # print(rb.returncode) assert rb.returncode == util.ReturnCode.OK msg = ( f"CONNECTED\n\n{ util.make_hyperlink('open_drive', 'Open') }" ) self.end(util.rich_text(msg)) elif task == 'connect_login': if not rb.returncode == util.ReturnCode.OK: self.pageLogin.showError(rb.error) self.progressBar.setVisible(False) elif rb.drive_status == 'CONNECTED': link = util.make_hyperlink('open_drive', 'Open') msg = util.rich_text(f"{rb.drive_status}\n\n{link}") self.end(msg) else: msg = rb.drive_status if rb.error: msg = rb.error self.end(msg) elif (task == 'disconnect' or task == 'repair'): self.end(rb.drive_status) elif task == 'restart_explorer': link = util.make_hyperlink('open_drive', 'Open') msg = util.rich_text(f"{rb.output}\n\n{link}") self.end(msg) else: msg = rb.output if rb.error: msg = rb.error self.end(msg) self.setPbConnectText(rb.drive_status) @pyqtSlot(bool) def onCheckUpdateDone(self, result): logger.info('Check update done') if result: link = util.make_hyperlink('update', 'Update and re-launch now') msg = util.rich_text(f"New version available\n\n{link}") self.end(msg) def onConfigFileChanged(self, path): logger.info('Config file has changed, reloading...') self.config = util.load_config(path) self.updateCombo(self.settings.value("cboParam", 0)) self.fillParam() self.lblUserHostPort.setText(self.param['userhostport']) self.checkDriveStatus() def updateCombo(self, currentIndex): items = [] drives = self.config.get('drives', '') for d in drives: items.append(f" {d} {drives[d].get('drivename','GOLDDRIVE')}") self.cboParam.blockSignals(True) self.cboParam.clear() self.cboParam.addItems(items) if currentIndex > len(items) - 1: currentIndex = 0 self.cboParam.setCurrentIndex(currentIndex) self.cboParam.blockSignals(False) def fillParam(self): p = self.param p['editor'] = self.config.get('editor') p['logfile'] = self.config.get('logfile') p['configfile'] = self.configfile p['user'] = getpass.getuser() p['appkey'] = util.get_app_key(p['user']) p['userhostport'] = '' default_port = 22 p['port'] = default_port p['drive'] = '' p['drivename'] = 'GOLDDRIVE' drives = self.config.get('drives') p['drives'] = drives p['no_host'] = not bool(drives) if not drives: return currentText = self.cboParam.currentText() if not currentText: return drive = currentText.split()[0].strip() d = drives[drive] p['drive'] = drive p['drivename'] = d.get('drivename', 'GOLDDRIVE').replace(' ', '-') p['host'] = d.get('hosts', 'localhost')[0] p['port'] = d.get('port', default_port) p['user'] = d.get('user', getpass.getuser()) p['userhost'] = f"{p['user']}@{p['host']}" p['userhostport'] = f"{p['userhost']}:{p['port']}" p['appkey'] = util.get_app_key(p['user']) p['args'] = d.get('args', '') # decorator used to trigger only the int overload and not twice @pyqtSlot(int) def on_cboParam_currentIndexChanged(self, e): # print(f'index changed: {e}') cbotext = self.cboParam.currentText() if not cbotext: return self.fillParam() self.lblUserHostPort.setText(self.param['userhostport']) if self.loaded: self.settings.setValue("cboParam", self.cboParam.currentIndex()) self.checkDriveStatus() def checkDriveStatus(self): if not self.param.get('drive'): return self.start(f"Checking {self.param['drive']}...") self.worker.doWork('check_drive', self.param) def onConnectHost(self, drive, userhostport): self.progressBar.setVisible(True) self.param['drive'] = drive userhost = userhostport host = userhostport self.param['userhost'] = userhost self.param['host'] = host self.param['port'] = 22 if ':' in userhostport: userhost, port = userhostport.split(':') self.param['port'] = port self.param['host'] = userhost host = userhost if '@' in userhost: user, host = userhost.split('@') self.param['user'] = user self.param['host'] = host else: user = self.param['user'] self.param['userhost'] = f'{user}@{host}' self.param['appkey'] = util.get_app_key(user) # print(f"user: {self.param['user']}") # print(f"host: {self.param['host']}") # print(f"port: {self.param['port']}") # print(f"userhost: {self.param['userhost']}") self.worker.doWork('connect', self.param) def onConnectLogin(self, password): self.progressBar.setVisible(True) self.param['password'] = password self.worker.doWork('connect_login', self.param) def mnuConnect(self): if self.param['no_host']: self.showPage(util.Page.HOST) else: self.start('Connecting drive...') self.worker.doWork('connect', self.param) def mnuDisconnect(self): self.start('Disconnecting drive...') self.worker.doWork('disconnect', self.param) def mnuDisconnectAll(self): self.start('Disconnecting all...') self.worker.doWork('disconnect_all', self.param) def mnuOpenProgramLocation(self): editor = self.param["editor"] if not os.path.exists(editor): editor = 'C:\\windows\\notepad.exe' cmd = fr'start /b c:\windows\explorer.exe "{util.DIR}\.."' util.run(cmd, shell=True) def mnuOpenTerminal(self): util.run('start %ComSpec%', shell=True) def mnuOpenLogFile(self): editor = self.param["editor"] if not os.path.exists(editor): editor = 'C:\\windows\\notepad.exe' logfile = self.param['logfile'] cmd = fr'start /b "" "{editor}" "{logfile}"' util.run(cmd, shell=True) def mnuRestartExplorer(self): self.start('Restarting explorer...') self.worker.doWork('restart_explorer', self.param) def mnuAbout(self): self.showPage(util.Page.ABOUT) self.pageAbout.showAbout() def closeEvent(self, event): self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) QMainWindow.closeEvent(self, event) self.worker.stop() self.updater.stop() def on_pbConnect_released(self): task = self.pbConnect.text().lower().replace('&', '') if task == 'connect': self.mnuConnect() else: self.mnuDisconnect() def on_lblSettings_linkActivated(self, link): editor = self.param["editor"] if not os.path.exists(editor): editor = 'C:\\windows\\notepad.exe' cmd = fr'start /b "" "{editor}" "{util.DIR}\..\config.json' util.run(cmd, shell=True) def on_lblMessage_linkActivated(self, link): if link == 'open_drive': drive = self.param['drive'] cmd = fr'start /b c:\windows\explorer.exe {drive}' util.run(cmd, shell=True) elif link == 'update': self.updater.doUpdate() elif link == 'install_winfsp': cmd = fr'start /b { self.config["winfsp_url"] }' util.run(cmd, shell=True) def showPage(self, page): if page == util.Page.HOST: self.pageHost.init() if page == util.Page.LOGIN: self.pageLogin.init() self.setTopEnabled(page == util.Page.MAIN) self.stackedWidget.setCurrentIndex(page.value) # print(f'panel visible: {page}') def setTopEnabled(self, enable): self.pbHamburger.setEnabled(enable) self.cboParam.setVisible(enable) self.cboParam.setEnabled(enable and self.cboParam.count() > 0) self.lblSettings.setVisible(enable) def keyPressEvent(self, event): if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter: # print('enter pressed in app') self.on_pbConnect_released() event.accept()
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 PreviewTab(QWidget): """Preview of QML component given by 'source' argument. If any file in the source's directory or one of its subdirectories is changed, the view is updated unless paused. Potential errors in the QML code are displayed in red.""" def __init__(self, source=None, parent=None, import_paths=None): super().__init__(parent=parent) self.updating_paused = False self.qml_view = QQuickView() engine = self.qml_view.engine() import_paths = import_paths or [] for import_path in import_paths: engine.addImportPath(import_path) # idea from # https://www.ics.com/blog/combining-qt-widgets-and-qml-qwidgetcreatewindowcontainer self.container = QWidget.createWindowContainer(self.qml_view, self) self.error_info = QLabel() self.error_info.setWordWrap(True) self.qml_source = QUrl() if source is None else QUrl(source) self.update_source() self.pause_button = QPushButton("Pause") self.pause_button.setCheckable(True) layout = QVBoxLayout() layout.setAlignment(Qt.AlignCenter) layout.addWidget(self.error_info) layout.addWidget(self.container) layout.addWidget(self.pause_button) self.setLayout(layout) # Observations using the QFileSystemWatcher in various settings: # A) fileChanged signal and qml file paths # Collected all *.qml files in the directory of the source and all # subdirectories and added to the watcher. Now the first change would # trigger the signal but none of the following # B) additionally connecting directoryChanged signal # same issue # C) both signals, (sub)directory file paths # Collected all subdirectories of the source directory and added it to # the watcher, along with the source directory itself. Works as # expected # D) directoryChanged signal, (sub)directory file paths # same as C) without fileChanged signal, works as expected # Eventual solution: D # This implementation also helped me: # https://github.com/penk/qml-livereload/blob/master/main.cpp self.watcher = QFileSystemWatcher() source_dir = os.path.dirname(source) source_paths = glob.glob(os.path.join(source_dir, "*/"), recursive=True) source_paths.append(source_dir) failed_paths = self.watcher.addPaths(source_paths) if failed_paths: print("Failed to watch paths: {}".format(", ".join(failed_paths))) self.pause_button.clicked.connect(self.toggle_updating) self.qml_view.statusChanged.connect(self.check_status) self.watcher.directoryChanged.connect(self.update_source) def toggle_updating(self, clicked): """Callback for pause button.""" self.pause_button.setText("Resume" if clicked else "Pause") self.updating_paused = clicked # Update when resuming in case of the source having changed if not self.updating_paused: self.update_source() def update_source(self, _=None): """Method and callback to update the QML view source. The second argument is required to have a slot matching the directoryChanged() interface. This immediately returns if updating is paused. """ if self.updating_paused: return # idea from # https://stackoverflow.com/questions/17337493/how-to-reload-qml-file-to-qquickview self.qml_view.setSource(QUrl()) self.qml_view.engine().clearComponentCache() # avoid error: No such file or directory QThread.msleep(50) self.qml_view.setSource(self.qml_source) self.container.setMinimumSize(self.qml_view.size()) self.container.setMaximumSize(self.qml_view.size()) # avoid error label making the window too wide self.error_info.setMaximumWidth(self.container.maximumWidth()) def check_status(self, status): if status == QQuickView.Error: self.error_info.setText("<font color='red'>{}</font>".format( self.qml_view.errors()[-1].toString())) else: self.error_info.clear() def shutdown(self): """Shutdown tab to prepare for removal. Manually delete QML related members to free resources. Otherwise error messages are printed even after closing the tab, indicating that the QML engine still runs in the background. """ del self.container del self.qml_view
class DatasetWidget(QWidget): def __init__(self): super().__init__() self.ui = Ui_Dataset() self.ui.setupUi(self) self.all_thumbnails = [] self.selected_thumbnails: Set[Thumbnail] = set() self.ui.image_list_widget.itemSelectionChanged.connect( self.on_changed_image_list_selection) self.ui.delete_images_button.clicked.connect( self.on_clicked_delete_images_button) self.ui.train_button.clicked.connect(self.on_clicked_train_button) self.ui.camera_and_images_menu = QMenu() self.ui.camera_and_images_menu.addAction(self.ui.select_images_action) self.ui.camera_and_images_menu.addAction(self.ui.camera_action) self.ui.camera_and_images_button.setMenu( self.ui.camera_and_images_menu) self.ui.select_images_action.triggered.connect( self.on_clicked_select_images_button) self.ui.camera_action.triggered.connect(self.on_clicked_camera_button) self.ui.image_list_widget.setCurrentItem( self.ui.image_list_widget.topLevelItem(0).child( 0)) # FIXME: refactor self.ui.image_list_widget.expandAll() self._reload_images(Dataset.Category.TRAINING_OK) self.__reload_recent_training_date() self.capture_dialog: Optional[ImageCaptureDialog] = None self.preview_window = PreviewWindow() self.watcher = QFileSystemWatcher(self) self.watcher.addPaths([ str(Dataset.images_path(Dataset.Category.TRAINING_OK)), str(Dataset.images_path(Dataset.Category.TEST_OK)), str(Dataset.images_path(Dataset.Category.TEST_NG)) ]) self.watcher.directoryChanged.connect( self.on_dataset_directory_changed) self.select_area_dialog = None self.msgBox = None LearningModel.default().training_finished.connect( self.on_finished_training) def _reload_images(self, category: Dataset.Category): # reset selection self.selected_thumbnails.clear() self.ui.delete_images_button.setEnabled(False) # reset grid area contents current_images_count = self.ui.images_grid_area.count() if current_images_count > 0: for i in reversed(range(current_images_count)): self.ui.images_grid_area.itemAt(i).widget().setParent(None) image_paths = sorted(Dataset.images_path(category).iterdir()) nullable_thumbnails = [ Thumbnail(path=image_path) for image_path in image_paths ] self.all_thumbnails = [ thumbnail for thumbnail in nullable_thumbnails if not thumbnail.pixmap.isNull() ] self.ui.number_of_images_label.setText(f'{len(self.all_thumbnails)}枚') row = 0 column = 0 for thumbnail in self.all_thumbnails: thumbnail_cell = ThumbnailCell(thumbnail=thumbnail) thumbnail_cell.selection_changed.connect( self.on_changed_thumbnail_selection) thumbnail_cell.double_clicked.connect( self.on_double_clicked_thumbnail) self.ui.images_grid_area.addWidget(thumbnail_cell, row, column) if column == 4: row += 1 column = 0 else: column += 1 def on_changed_image_list_selection(self): selected_category = self.__selected_dataset_category() if selected_category is not None: self._reload_images(selected_category) def on_changed_thumbnail_selection(self, selected: bool, thumbnail: Thumbnail): if selected: self.selected_thumbnails.add(thumbnail) else: self.selected_thumbnails.remove(thumbnail) if self.selected_thumbnails: number_of_images_description = f'{len(self.all_thumbnails)}枚 - {len(self.selected_thumbnails)}枚選択中' self.ui.delete_images_button.setEnabled(True) else: number_of_images_description = f'{len(self.all_thumbnails)}枚' self.ui.delete_images_button.setEnabled(False) self.ui.number_of_images_label.setText(number_of_images_description) def on_double_clicked_thumbnail(self, thumbnail: Thumbnail): self.preview_window.set_thumbnail(thumbnail) self.preview_window.show() self.preview_window.activateWindow() self.preview_window.raise_() # move preview to center preview_geometry: QRect = self.preview_window.frameGeometry() screen_center = QDesktopWidget().availableGeometry().center() preview_geometry.moveCenter(screen_center) self.preview_window.move(preview_geometry.topLeft()) def on_clicked_camera_button(self): selected_category = self.__selected_dataset_category() if selected_category is None: print('TODO: disable to select other items') return del self.capture_dialog self.capture_dialog = ImageCaptureDialog( image_save_location=str(Dataset.images_path(selected_category))) self.capture_dialog.show() def on_clicked_select_images_button(self): selected_category = self.__selected_dataset_category() if selected_category is None: print('TODO: disable to select other items') return ext_filter = '画像ファイル(*.jpg *.jpeg *.png *.gif *.bmp)' source_image_names = QFileDialog.getOpenFileNames( caption='データセットに取り込む', filter=ext_filter, directory=Project.latest_dataset_image_path())[0] Project.save_latest_dataset_image_path( os.path.dirname(source_image_names[0])) if source_image_names: for source_image_name in source_image_names: try: # TODO: specify correct camera number destination = Dataset.generate_image_path( category=selected_category, cam_number=0, file_extension=Path(source_image_name).suffix) shutil.copyfile(source_image_name, destination) except shutil.SameFileError: print("TODO: fix destination") def on_clicked_delete_images_button(self): assert self.selected_thumbnails message = f'{len(self.selected_thumbnails)}枚の画像を削除してよろしいですか?\nこの操作は取り消せません' selected_action = QMessageBox.warning(None, '', message, QMessageBox.Cancel, QMessageBox.Yes) if selected_action == QMessageBox.Yes: for selected_thumbnail in self.selected_thumbnails: os.remove(path=str(selected_thumbnail.path)) self._reload_images(self.__selected_dataset_category()) def on_clicked_train_button(self): img_suffix_list = ['.jpg', '.jpeg', '.png', '.gif', '.bmp'] if not [ img for img in os.listdir( Dataset.images_path(Dataset.Category.TEST_NG)) if Path(img).suffix in img_suffix_list ]: self.msgBox = QMessageBox() self.msgBox.setText( '性能評価用の不良品画像フォルダが空です.\nトレーニングを開始するには不良品画像を1枚以上追加してください.') self.msgBox.exec() return elif not [ img for img in os.listdir( Dataset.images_path(Dataset.Category.TEST_OK)) if Path(img).suffix in img_suffix_list ]: self.msgBox = QMessageBox() self.msgBox.setText( '性能評価用の良品画像フォルダが空です.\nトレーニングを開始するには良品画像を1枚以上追加してください.') self.msgBox.exec() return elif not [ img for img in os.listdir( Dataset.images_path(Dataset.Category.TRAINING_OK)) if Path(img).suffix in img_suffix_list ]: self.msgBox = QMessageBox() self.msgBox.setText( 'トレーニング用の良品画像フォルダが空です.\nトレーニングを開始するには良品画像を1枚以上追加してください.') self.msgBox.exec() return del self.select_area_dialog self.select_area_dialog = SelectAreaDialog() self.select_area_dialog.finish_selecting_area.connect( self.on_finished_selecting_area) self.select_area_dialog.show() self.__reload_recent_training_date() def on_finished_selecting_area(self, data: TrimmingData): categories = [ Dataset.Category.TRAINING_OK, Dataset.Category.TEST_OK, Dataset.Category.TEST_NG ] truncated_image_paths = [] for category in categories: dir_path = Dataset.images_path(category) save_path = Dataset.trimmed_path(category) if os.path.exists(save_path): shutil.rmtree(save_path) os.mkdir(save_path) if not data.needs_trimming: copy_tree(str(dir_path), str(save_path)) else: file_list = os.listdir(dir_path) file_list = [ img for img in file_list if Path(img).suffix in ['.jpg', '.jpeg', '.png', '.gif', '.bmp'] ] for file_name in file_list: truncated_image_path = Dataset.trim_image( os.path.join(dir_path, file_name), save_path, data) if truncated_image_path: file_name = os.path.basename(truncated_image_path) shutil.move( truncated_image_path, os.path.join( Dataset.images_path( Dataset.Category.TRUNCATED), file_name)) truncated_image_paths.append(truncated_image_path) Project.save_latest_trimming_data(data) # alert for moving truncated images if truncated_image_paths: self.msgBox = QMessageBox() self.msgBox.setText(str(len(truncated_image_paths))+'枚の画像を読み込めませんでした. これらの画像はtruncatedフォルダに移動されました.\n\n'\ + 'このままトレーニングを開始しますか?') self.msgBox.setStandardButtons(self.msgBox.Yes | self.msgBox.No) self.msgBox.setDefaultButton(self.msgBox.Yes) reply = self.msgBox.exec() if reply == self.msgBox.No: return # start training LearningModel.default().start_training() def on_dataset_directory_changed(self, directory: str): selected_category = self.__selected_dataset_category() if str(Dataset.images_path(selected_category)) == directory: self._reload_images(selected_category) def on_finished_training(self): self.__reload_recent_training_date() def __selected_dataset_category(self) -> Optional[Dataset.Category]: current_item = self.ui.image_list_widget.currentItem() current_item_text = current_item.text(0) # FIXME: refactor if current_item_text == 'トレーニング用画像' or current_item_text == '性能評価用画像': return None elif current_item.parent().text(0) == 'トレーニング用画像': if current_item_text == '良品': # train_OK return Dataset.Category.TRAINING_OK elif current_item.parent().text(0) == '性能評価用画像': if current_item_text == '良品': # test_OK return Dataset.Category.TEST_OK elif current_item_text == '不良品': # test_NG return Dataset.Category.TEST_NG else: assert False def __reload_recent_training_date(self): latest_training_date = Project.latest_training_date() if latest_training_date is None: self.ui.latest_training_date_label.setText('トレーニング未実行') else: date_description = latest_training_date.strftime('%Y/%m/%d') self.ui.latest_training_date_label.setText( f'前回のトレーニング:{date_description}')