def getProfiles(shortGameName, zEditInstallFolder) -> List[Merges]: """ This returns the content of each profiles 'merges.json' """ result = [] zEditProfileDir = QDir(zEditInstallFolder + "/" + ZEditConfig.RELATIVE_PROFILE_DIR) if not zEditProfileDir.exists(): qDebug("Profiles path does not exist: {}".format( zEditProfileDir.absolutePath())) return result profiles = ZEditConfig.parseProfileList(shortGameName) for name in profiles: relName = name + "/merges.json" if not zEditProfileDir.exists(relName): continue try: filePath = zEditProfileDir.absoluteFilePath(relName) with open(filePath) as f: m = Merges(json.load(f)) m.profileName = name m.profilePath = filePath result.append(m) except ValueError as ex: qWarning('Invalid file "{}": {}'.format(filePath, str(ex))) return result
def __load(self): """ Private slot to load the available scripts into the manager. """ scriptsDir = QDir(self.scriptsDirectory()) if not scriptsDir.exists(): scriptsDir.mkpath(self.scriptsDirectory()) if not scriptsDir.exists("requires"): scriptsDir.mkdir("requires") self.__disabledScripts = Preferences.getWebBrowser( "GreaseMonkeyDisabledScripts") from .GreaseMonkeyScript import GreaseMonkeyScript for fileName in scriptsDir.entryList(["*.js"], QDir.Files): absolutePath = scriptsDir.absoluteFilePath(fileName) script = GreaseMonkeyScript(self, absolutePath) if not script.isValid(): del script continue self.__scripts.append(script) if script.fullName() in self.__disabledScripts: script.setEnabled(False) else: collection = WebBrowserWindow.webProfile().scripts() collection.insert(script.webScript()) self.__jsObject.setSettingsFile( os.path.join(Utilities.getConfigDir(), "web_browser", "greasemonkey_values.ini")) ExternalJsObject.registerExtraObject("GreaseMonkey", self.__jsObject)
def copyFiles(self, src, dst, names=None, overwrite=True): """ Copy files from src directory to dst directory. Subfolders are ignored. If names is provided, copies only files matching those names. Symlinks are not copied """ moDebug("Copying files from {} to {}. Names: {}".format( src, dst, names)) srcDir = QDir(src, "", QDir.IgnoreCase, QDir.Files | QDir.NoSymLinks) dstDir = QDir(dst) if not srcDir.exists() or not dstDir.exists(): moWarn("Failed to copy files with non-existant directories") return False success = True if names: srcDir.setNameFilters(names) fileNames = srcDir.entryList() for name in fileNames: if dstDir.exists(name): if not overwrite: continue dstDir.remove(name) success = QFile.copy(srcDir.filePath(name), dstDir.filePath(name)) if not success: moWarn('Failed to copy "{}" from "{}" to "{}"'.format( name, src, dst)) return success
def __load(self): """ Private slot to load the available scripts into the manager. """ scriptsDir = QDir(self.scriptsDirectory()) if not scriptsDir.exists(): scriptsDir.mkpath(self.scriptsDirectory()) if not scriptsDir.exists("requires"): scriptsDir.mkdir("requires") self.__disabledScripts = \ Preferences.getHelp("GreaseMonkeyDisabledScripts") from .GreaseMonkeyScript import GreaseMonkeyScript for fileName in scriptsDir.entryList(["*.js"], QDir.Files): absolutePath = scriptsDir.absoluteFilePath(fileName) script = GreaseMonkeyScript(self, absolutePath) if script.fullName() in self.__disabledScripts: script.setEnabled(False) if script.startAt() == GreaseMonkeyScript.DocumentStart: self.__startScripts.append(script) else: self.__endScripts.append(script)
def loadProfile(shortGameName, profileName, zEditInstallFolder) -> Merges: zEditProfileDir = QDir(zEditInstallFolder + "/" + ZEditConfig.RELATIVE_PROFILE_DIR) if not zEditProfileDir.exists(): moWarn("Profiles path does not exist: {}".format( zEditProfileDir.absolutePath())) return profiles = ZEditConfig.parseProfileList(shortGameName) for name in profiles: if name == profileName: relName = name + "/merges.json" if not zEditProfileDir.exists(relName): moWarn('Profile "{}" does not have a "merges.json" file.'. format(name)) return try: filePath = zEditProfileDir.absoluteFilePath(relName) with open(filePath) as f: m = Merges(json.load(f)) m.profileName = name m.profilePath = filePath return m except ValueError as ex: moWarn( 'Failed to read zEdit profile. Invalid file: "{}": {}'. format(filePath, str(ex))) moError('Profile "{}" was not found'.format(profileName)) return Merges()
def findStampFileName(name, currentFileName=''): invalidChars = QRegularExpression("[^\\w -]+") prefs = preferences.Preferences.instance() stampsDir = QDir(prefs.stampsDirectory()) suggestedFileName = name.toLower().remove(invalidChars) fileName = suggestedFileName + ".stamp" if (fileName == currentFileName or not stampsDir.exists(fileName)): return fileName n = 2 while (fileName != currentFileName and stampsDir.exists(fileName)): fileName = suggestedFileName + QString.number(n) + ".stamp" n += 1 return fileName
def validate_project(project_dir): selected_dir = QDir(project_dir) if not selected_dir.exists(): QMessageBox.critical(None, 'Dir does not exist', 'Dir does not exist', QMessageBox.Yes, QMessageBox.Yes) return False if not selected_dir.exists('src/global.config'): QMessageBox.critical(None, 'Project validation error', 'Project dir does not contain src/global.config', QMessageBox.Yes, QMessageBox.Yes) return False return True
def createBackupFolder(self, profileName: str): """ Create a backup folder in the profile. The profile must exist """ profileDir = QDir(self.profilePath(profileName)) if not profileDir.exists(): raise ValueError( "Attempted to create backup folder in non-existant profile: {}" .format(profileName)) if profileDir.exists(self.PROFILE_DIR): return True if not profileDir.mkdir(self.PROFILE_DIR): moWarn("Failed to create backup directory in profile: {}".format( profileName)) return False return True
def requireScripts(self, urlList): """ Public method to get the sources of all required scripts. @param urlList list of URLs (list of string) @return sources of all required scripts (string) """ requiresDir = QDir(self.requireScriptsDirectory()) if not requiresDir.exists() or len(urlList) == 0: return "" script = "" settings = QSettings( os.path.join(self.requireScriptsDirectory(), "requires.ini"), QSettings.IniFormat) settings.beginGroup("Files") for url in urlList: if settings.contains(url): fileName = settings.value(url) try: f = open(fileName, "r", encoding="utf-8") source = f.read() f.close() except (IOError, OSError): source = "" script += source.strip() + "\n" return script
def accept(self) -> None: """Checks if a specified directory is empty. If it isn't warns the user. If the directory doesn't exist attempts to create it""" accept = True path = self.ui.LocationLineEdit.text() directory = QDir(path) if directory.exists(): if not directory.isEmpty(): reply = QMessageBox.question(self, "Create project?", "The directory you specified is not empty." + "Are you sure that you want to create the project there?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: accept = False else: try: directory.mkpath(self.ui.LocationLineEdit.text()) except OSError as e: accept = False QMessageBox.critical(self, "Error", f"Failed to create project folder: {str(e)}") self.path = self.ui.LocationLineEdit.text() self.authors = self.ui.AuthoursLineEdit.text() self.project_type = self.ui.ProjectTypeComboBox.currentText() self.title = self.ui.TitleLineEdit.text() if accept: super().accept()
def initUndoRedo(self): self.undoStack = QUndoStack() temp = QDir(os.path.join(QDir.tempPath(), "FlatSiteBuilder")) if temp.exists(): temp.removeRecursively() temp.setPath(QDir.tempPath()) temp.mkdir("FlatSiteBuilder")
def onMapsDirectoryChanged(self): prefs = preferences.Preferences.instance() mapsDir = QDir(prefs.mapsDirectory()) if (not mapsDir.exists()): mapsDir.setPath(QDir.currentPath()) self.model().setRootPath(mapsDir.canonicalPath()) self.setRootIndex(self.model().index(mapsDir.absolutePath()))
def initUndoRedo(self): self.undoStack = QUndoStack() temp = QDir(QDir.tempPath() + "/AppBuilder") if temp.exists(): temp.removeRecursively() temp.setPath(QDir.tempPath()) temp.mkdir("AppBuilder")
def file_less_suffix(FBTS): qd = QDir( FBTS.folderpath() ) qd.setFilter(QDir.Files | QDir.Readable) if qd.exists(FBTS.basename()): a_file = QFile( qd.absoluteFilePath(FBTS.basename()) ) return _qfile_to_stream(a_file, FBTS.open_mode()) return None
def loadInImageViewer(self, path): dir = QDir(path) if dir.exists(): entryList = dir.entryList(['*.jpg'], QDir.Files) if len(entryList) > 0: # load in thumb viewer # QMessageBox.information(None, u"Begehung", u",".join(entryList)) imagePathList = [] for image in entryList: imagePathList.append(path + u'\\' + image) widget = APISThumbViewer() widget.load(imagePathList) widget.show() if widget.exec_(): pass # app.exec_() else: QMessageBox.information( self, u"Begehung", u"Es wurden keine Dateien [*.jpg] für diesen Fundort gefunden." ) else: QMessageBox.information( self, u"Begehung", u"Das Verzeichnis '{0}' wurde nicht gefunden.".format(path))
def __init__(self): QMainWindow.__init__(self) self.site = None self.editor = "" self.install_directory = os.getcwd() self.content_after_animation = "" self.default_path = "" self.method_after_animation = "" Generator.install_directory = self.install_directory self.initUndoRedo() self.initGui() self.readSettings() self.loadPlugins() if self.default_path: if self.loadProject(self.default_path + "/Site.qml"): # if site has never been generated (after install) # then generate the site site = QDir(Generator.sitesPath() + "/" + self.site.title) if site.exists(): gen = Generator() gen.generateSite(self, self.site) self.dashboard.setExpanded(True) self.showDashboard() self.statusBar().showMessage("Ready")
def Seal(dir_path): """ Seal a folder: count recursively MD5 checksum, write it to all.md5, then make the folder read-only. Shell function, need package md5deep. """ METHOD_NAME = f"Blister.Seal" qdir = QDir(dir_path) if not qdir.exists(): print(f"{METHOD_NAME}: Path doesn't exist or isn't a dir.", end='\n') return False qfiles = QFileInfo(qdir, "**/*") file_list = glob.glob(qfiles.absoluteFilePath(), recursive=True) for _file in file_list: fileinfo = QFileInfo(_file) if not fileinfo.isReadable(): print(f"{METHOD_NAME}: There is input cannot be checked (unreadable files). Stopped.", end='\n') return False start_time = time.time() sp = subprocess.Popen(f"cd {qdir.absolutePath()}; (md5deep -lr * > all.md5); chmod 555 -R {qdir.absolutePath()}", shell=True, stderr=subprocess.PIPE) out, err = sp.communicate() if err != b'': print(f"{METHOD_NAME}: Shell error: {str(err)}", end='\n') return False print(f"{METHOD_NAME}: Dir was sealed [%s]:\n\t{qdir.absolutePath()}" % (Blister.SecToTime(time.time() - start_time)), end='\n') return True
def getExportExtensionsList(self): extensions = [] for extsprefix in datadirs: extsdir = QDir(extsprefix+'/export-extensions/') if extsdir.exists(): for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'], QDir.Files | QDir.Readable): extensions.append(self.readExtension(fileInfo.filePath())) locale = QLocale.system().name() self.extensionActions = [] for extension in extensions: try: if ('Name[%s]' % locale) in extension: name = extension['Name[%s]' % locale] elif ('Name[%s]' % locale.split('_')[0]) in extension: name = extension['Name[%s]' % locale.split('_')[0]] else: name = extension['Name'] data = {} for prop in ('FileFilter', 'DefaultExtension', 'Exec'): if 'X-ReText-'+prop in extension: data[prop] = extension['X-ReText-'+prop] elif prop in extension: data[prop] = extension[prop] else: data[prop] = '' action = self.act(name, trig=self.extensionFunction(data)) if 'Icon' in extension: action.setIcon(self.actIcon(extension['Icon'])) mimetype = extension['MimeType'] if 'MimeType' in extension else None except KeyError: print('Failed to parse extension: Name is required', file=sys.stderr) else: self.extensionActions.append((action, mimetype))
def new_campaign(self): dialog = QFileDialog(self) dialog.setDirectory(helper.one_up(self.current_dir)) dialog.setAcceptMode(QFileDialog.AcceptSave) dialog.setFileMode(QFileDialog.AnyFile) dialog.setOption(QFileDialog.ShowDirsOnly) dialog.setWindowTitle('Select folder name') if dialog.exec_(): directory = dialog.selectedFiles()[0] qdir = QDir(directory) if not qdir.exists(): self.current_dir = qdir.path() for folder_name in resource.folders: qdir.mkpath('./' + folder_name) helper.save_json_data( '{}/{}/health{}'.format(self.current_dir, resource.health_stat.folder, resource.health_stat.ext), resource.health_stat.default) qdir.mkpath('./.settings/std') qdir.mkpath('./.settings/debug') resource.create_config_files(self.current_dir, qdir.dirName()) self.refresh_tree_view() else: helper.display_error('Directory for campaign already exists.' ) # This shouldn't happen
def scan_for_csl_styles(self) -> dict: """Scans <app data>/csl_styles subdirectories for csl styles. Returns a dictionary with information about the styles that were found""" # Get all files in the csl_styles directory path = self.get_appdata_path() + "/csl_styles" directory = QDir(path) # Create directory if it doesn't exist if not directory.exists(): try: directory.mkpath(path) except Exception: print(f"Failed to create path: {path}") return dict() entries = directory.entryList(QDir.Files) # Iterate over files styles = dict() for entry in entries: if entry.endswith(".csl"): filepath = path + "/" + entry title = self.extract_csl_style_name(filepath) identifier = common.generate_identifier(title) if title is not None: styles[identifier] = {"name": title, "path": filepath} return styles
def parseProfileList(shortGameName: str): # the profile list is stored in Roaming/zEdit/profiles.json result = [] gameMode = ZEditConfig.getZEditGameMode(shortGameName) if gameMode is None: qWarning("Game type is not supported by zMerge.") return result appData = QDir( QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) profilesPath = appData.absoluteFilePath( ZEditConfig.RELATIVE_ZEDIT_PROFILES_FILE) if not appData.exists(ZEditConfig.RELATIVE_ZEDIT_PROFILES_FILE): qDebug('"{}" does not exist'.format(profilesPath)) return result try: file = open(profilesPath, "r", encoding="utf8") profiles = json.load(file) for profile in profiles: if profile["gameMode"] == gameMode: result.append(profile["name"]) except OSError: qWarning('Failed to read "profiles.json".') return [] except (TypeError, ValueError): qWarning('"profiles.json" has unknown file structure.') return result
def initUI(self): self.setStyleSheet('background-color: silver') image_folder = QDir('images') if not image_folder.exists(): logging.error('Image foulder not found') sys.exit(1) self.layout_h = QHBoxLayout() self.scheduler = MasterTool(self, 'BinanceSched', 1) self.scheduler.setPixmap( QPixmap('images/BinanceSched.png').scaled(120, 60)) self.ohlc = MasterTool(self, 'BinanceOHLC', 1) self.ohlc.setPixmap(QPixmap('images/BinanceOHLC.png').scaled(120, 60)) self.order = MasterTool(self, 'BinanceOrder', 1) self.order.setPixmap( QPixmap('images/BinanceOrder.png').scaled(120, 60)) self.layout_h.addWidget(self.scheduler) self.layout_h.addWidget(self.ohlc) self.layout_h.addWidget(self.order) self.layout_h.addStretch(1) self.setLayout(self.layout_h)
def file_less_suffix(FBTS): qd = QDir(FBTS.folderpath()) qd.setFilter(QDir.Files | QDir.Readable) if qd.exists(FBTS.basename()): a_file = QFile(qd.absoluteFilePath(FBTS.basename())) return _qfile_to_stream(a_file, FBTS.open_mode()) return None
def __init__(self, parent=None): super(MainApp, self).__init__(parent) self.setupUi(self) #init statusbar self.setStyleSheet("QStatusBar::item { border: 0px solid black }; ") self.statusBar = QLabel() self.statusbar.addWidget(self.statusBar) self.statusBar.setText("No configuration file loaded.") #object variables self.currentPlugin = False self.pluginContainer = None self.selectedElement = None self.unsavedContent = False self.editorContainer = None self.ignoreWidgetsToSettingsData = False self.fileHandler = None self.lastFile = "" #buttons self.button_add.setEnabled(False) self.button_del.setEnabled(False) #signals self.actionLoad_Configuration.triggered.connect(self.loadFileDialog) self.actionSave_Configuration.triggered.connect(self.saveFile) self.actionSave_Configuration_File_As.triggered.connect(self.saveFileAs) self.actionNew_Configuration_File.triggered.connect(self.newFile) self.actionExit.triggered.connect(self.closeEventButton) self.actionAbout_This_Program.triggered.connect(self.showAboutWidget) self.button_add.clicked.connect(self.showAddSettingWidget) self.button_del.clicked.connect(self.deleteSetting) #treemodel self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.treeViewOpenMenu) self.treeView.mousePressEvent = MethodType(mousePressEvent, self.treeView) self.activeModel = SettingTree(self) self.proxyModel = FilterProxyModel(self) self.proxyModel.setSourceModel(self.activeModel) self.treeView.setModel(self.proxyModel) self.treeView.setColumnHidden(1, True) self.completeModel = SettingTree(HeaderData(True), self) #plugin content self.contentXML = None #icon self.icon = "" dataDir = QDir(os.path.dirname(os.path.realpath(__file__))) dataDir.cd("data") if dataDir.exists("icon.png"): self.icon = dataDir.absoluteFilePath("icon.png") self.setWindowIcon(QIcon(self.icon)) #programm configuration for window size and last filename self.programConfig = ProgramConfiguration(self) self.programConfig.loadConfiguration()
def get_cwd(): """Get the current working directory (default for file open and save-as)""" settings = QSettings() cwd = QDir(settings.value(Settings.CWD, defaultValue=Settings.CWD_DFLT)) if not cwd.exists(): cwd = QDir(str(Path.home())) return cwd.canonicalPath() # resolve symlinks and relative paths
def removeDir(dirName): d = QDir(dirName) if d.exists(): for info in d.entryInfoList(QDir.Dirs | QDir.Files | QDir.NoDotAndDotDot): if info.isDir(): removeDir(info.absoluteFilePath()) else: d.remove(info.fileName()) d.rmdir(dirName)
def tempDirCreate(self, basedir, name=None): tmpdir = QDir(basedir) if not tmpdir.exists(): return uid = name if name else str(uuid.uuid4()) path = tmpdir.absoluteFilePath(uid) if tmpdir.mkpath(path): return path
def run(self): watch_dir = QDir(self.path) if self.path and watch_dir.exists(): self.running = True event_handler = Handler() event_handler.store = self.store self.observer.schedule(event_handler, self.path, recursive=True) self.observer.start() else: Logger.Logger().debug( 'Error no path set, or path is invalid {}'.format(self.path))
def getSettingsFile(): appPath = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) appDir = QDir(appPath) if not appDir.exists(): logger.debug('Creating app directory') appDir.mkpath(appDir.absolutePath()) else: logger.debug('App directory already exists') if appDir.setCurrent(appDir.absolutePath()): logger.debug('Changing cwd to app data directory') else: logger.debug('Changing cwd to app data directory failed') return QFile('settings.json')
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")
def getpath(self): self.tableWidget.setRowCount(0) path = self.lineEdit.text() if path != "": dir = QDir() if not dir.exists(path): dir.mkdir(path) dir = QDir(path) flag = 0 for d in dir.entryList(QDir.Dirs | QDir.NoDotAndDotDot): self.tableWidget.insertRow(flag) self.tableWidget.setItem(flag, 0, QtWidgets.QTableWidgetItem(os.path.join(path, d))) flag += 1
def preloadMimeIcons(): icons = {} mIconsDir = QDir(':/share/icons/mimetypes') if not mIconsDir.exists(): return icons entries = mIconsDir.entryList() for entry in entries: mType = entry.replace('.png', '').replace('-', '/', 1) icons[mType] = getIcon('mimetypes/{}'.format(entry)) return icons
def Successful_Tmp(self):# CheckTmpDir, StateTmpDir failed = lambda: not self.tdir or not self.tdir.isValid() if failed():# not successfully self.tdir = QTemporaryDir() if failed(): QMessageBox.critical(self, "Unexpected Failurer", "The application has detected a problem trying to\nCreate or Access a Temporary File!.") return None else: self.tdir.setAutoRemove(True) d = QDir(self.tdir.path()) if not d.exists("ninja-ide"): if not d.mkdir("ninja-ide"): self.tdir = None d.cd("ninja-ide") return d
def __init__(self, mainWindow, parent = None): super().__init__(parent) self.mMainWindow = mainWindow self.setRootIsDecorated(False) self.setHeaderHidden(True) self.setItemsExpandable(False) self.setUniformRowHeights(True) self.setDragEnabled(True) self.setDefaultDropAction(Qt.MoveAction) prefs = preferences.Preferences.instance() prefs.mapsDirectoryChanged.connect(self.onMapsDirectoryChanged) mapsDir = QDir(prefs.mapsDirectory()) if (not mapsDir.exists()): mapsDir.setPath(QDir.currentPath()) self.mFSModel = FileSystemModel(self) self.mFSModel.setRootPath(mapsDir.absolutePath()) nameFilters = QStringList("*.tmx") # The file system model name filters are plain, whereas the plugins expose # a filter as part of the file description filterFinder = QRegExp("\\((\\*\\.[^\\)\\s]*)") for format in PluginManager.objects(MapFormat): if not (format.capabilities() & MapFormat.Read): continue filter = format.nameFilter() if (filterFinder.indexIn(filter) != -1): nameFilters.append(filterFinder.cap(1)) self.mFSModel.setFilter(QDir.AllDirs | QDir.Files | QDir.NoDot) self.mFSModel.setNameFilters(nameFilters) self.mFSModel.setNameFilterDisables(False) # hide filtered files self.setModel(self.mFSModel) headerView = self.header() headerView.hideSection(1) # Size column headerView.hideSection(2) headerView.hideSection(3) self.setRootIndex(self.mFSModel.index(mapsDir.absolutePath())) self.header().setStretchLastSection(False) self.header().setSectionResizeMode(0, QHeaderView.Stretch) self.activated.connect(self.onActivated) self.mMainWindow = None self.mFSModel = None
def saveStamp(self, stamp): # make sure we have a stamps directory prefs = preferences.Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDir = QDir(stampsDirectory) if (not stampsDir.exists() and not stampsDir.mkpath(".")): qDebug("Failed to create stamps directory" + stampsDirectory) return filePath = stampsDir.filePath(stamp.fileName()) file = QSaveFile(filePath) if (not file.open(QIODevice.WriteOnly)): qDebug("Failed to open stamp file for writing" + filePath) return stampJson = stamp.toJson(QFileInfo(filePath).dir()) file.write(QJsonDocument(stampJson).toJson(QJsonDocument.Compact)) if (not file.commit()): qDebug() << "Failed to write stamp" << filePath
def _scan(self, _): """ Invoked when the user clicks on the scan button. """ project = self.project package = self.package # Get the root directory to scan. root = self.get_root_dir() if root == '': return # Save the included state of any existing contents so that they can be # restored after the scan. old_state = {} for itm in self._get_items(): rel_path = [itm.data(0, Qt.DisplayRole)] parent = itm.parent() while parent is not None: rel_path.append(parent.data(0, Qt.DisplayRole)) parent = parent.parent() rel_path.reverse() if self._show_root: rel_path = rel_path[1:] old_state['/'.join(rel_path)] = (itm.checkState(0) == Qt.Checked) # Walk the package. root_dir = QDir(root) if not root_dir.exists(): QMessageBox.warning(self.parentWidget(), "Scan Directory", "{0} is not a valid directory.".format( QDir.toNativeSeparators(root))) return self._add_to_container(package, root_dir, [], old_state) self._visualise() self.package_changed.emit()
def search_dir(base_path, theme_name): """ Search for theme name """ # Search each entry in this directory base_dir = QDir(base_path) for e in base_dir.entryList(): # Path to current item path = base_dir.path() + "/" + e base_filename = e.split('.')[0] # If file matches theme name, return if base_filename == theme_name: return path # If this is a directory, search within it dir = QDir(path) if dir.exists(): # If found below, return it res = search_dir(path, theme_name) if res: return res # If no match found in dir, return None return None
def set_path(self,book_folder_path): book_dir = QDir(book_folder_path) if book_dir.exists('pngs') : self.png_dir = QDir(book_dir.absoluteFilePath('pngs')) self._enable() self.cursor_move()
class ImageDisplay(QWidget): def __init__(self, my_book, parent=None): super().__init__(parent) self.my_book = my_book # register metadata readers and writers md = my_book.get_meta_manager() md.register(C.MD_IZ,self._zoom_read,self._zoom_write) md.register(C.MD_IX,self._link_read,self._link_write) # Create our widgets including cursor_to_image and # image_to_cursor pushbuttons. self._uic() # set defaults in case no metadata self.cursor_to_image.setChecked(True) self.image_to_cursor.setChecked(False) self.zoom_factor = 0.25 self.png_path = None # disable all widgetry until we get some metadata self._disable() # end of __init__() # Disable our widgets because we have no image to show. def _disable(self): self.no_image = True self.last_index = None # compares unequal to any self.pix_map = QPixmap() self.image = QImage() self.cursor_to_image.setEnabled(False) self.image_to_cursor.setEnabled(False) self.zoom_pct.setEnabled(False) self.zoom_to_width.setEnabled(False) self.zoom_to_height.setEnabled(False) self.image_display.setPixmap(self.gray_image) self.image_display.setToolTip( _TR('Image view tooltip', 'Display of one scanned page (no images available)') ) # Enable our widgets, we have images to show. At this time the Book # has definitely created an edit view and a page model. def _enable(self): self.edit_view = self.my_book.get_edit_view() self.editor = self.edit_view.Editor # access to actual QTextEdit self.page_data = self.my_book.get_page_model() self.cursor_to_image.setEnabled(True) self.image_to_cursor.setEnabled(True) self.zoom_to_width.setEnabled(True) self.zoom_to_height.setEnabled(True) self.image_display.setToolTip( _TR('Image view tooltip', 'Display of one scanned page from the book') ) self.no_image = False self.zoom_pct.setEnabled(True) # the following triggers entry to _new_zoom_pct() below self.zoom_pct.setValue(int(100*self.zoom_factor)) # Metadata: read or write the {{IMAGEZOOM f}} section. # Parameter f should be a decimal number between 0.15 and 2.0 # but we do not depend on text the user could edit. def _zoom_read(self, qts, section, vers, parm): try: z = float(parm) # throws exception on a bad literal if math.isnan(z) or (z < 0.15) or (z > 2.0) : raise ValueError self.zoom_factor = z except: imageview_logger.error('Invalid IMAGEZOOM "{0}" ignored'.format(parm)) def _zoom_write(self, qts, section): qts << metadata.open_line(section, str(self.zoom_factor)) # Metadata: read or write the {{IMAGELINK b}} section. The parameter should # be an int 0/1/2/3. Bit 0 represents the state of cursor_to_image # (usually 1); bit 1 represents the state of image_to_cursor (usually 0). def _link_read(self, qts, section, vers, parm): try: b = int(parm) # exception on a bad literal if (b < 0) or (b > 3) : raise ValueError self.cursor_to_image.setChecked( True if b & 1 else False ) self.image_to_cursor.setChecked( True if b & 2 else False ) except : imageview_logger.error('Invalid IMAGELINKING "{0}" ignored'.format(parm)) def _link_write(self, qts, section): b = 0 if self.cursor_to_image.isChecked() : b |= 1 if self.image_to_cursor.isChecked() : b |= 2 qts << metadata.open_line(section, str(b)) # The Book calls here after it has loaded a book with defined page data, # passing the path to the folder containing the book. If we can find a # folder named 'pngs' in it we record that path and enable our widgets, # and fake a cursorMoved signal to display the current edit page. def set_path(self,book_folder_path): book_dir = QDir(book_folder_path) if book_dir.exists('pngs') : self.png_dir = QDir(book_dir.absoluteFilePath('pngs')) self._enable() self.cursor_move() # Come here to display or re-display an image. The last-displayed # page image index (if any) is in self.last_index. The desired page # index is passed as the argument, which may be: # * the same as last_index, for example on a change of zoom%. Just # redisplay the current page. # * negative or None if the cursor is "above" the first available page or on # a Page-Up keystroke. Display the gray image. # * greater than page_data.page_count() on a Page-Down keystroke, # display the last available page. # If different from last_index, try to load the .png file for that # page. If that fails, use the gray image. Otherwise display that # page and save it as last_index. def _show_page(self, page_index): if page_index != self.last_index : self.last_index = page_index # change of page, see if we have a filename for it self.pix_map = self.gray_image # assume failure... im_name = self.page_data.filename(page_index) if im_name : # pagedata has a filename; of course there is no guarantee # such a file exists now or ever did. im_name += '.png' if self.png_dir.exists(im_name) : self.image = QImage(self.png_dir.absoluteFilePath(im_name)) if not self.image.isNull(): # we loaded it ok, make a full-scale pixmap for display self.pix_map = QPixmap.fromImage(self.image,Qt.ColorOnly) # Whether new page or not, rescale to current zoom. The .resize method # takes a QSize; pix_map.size() returns one, and it supports * by a real. self.image_display.setPixmap(self.pix_map) self.image_display.resize( self.zoom_factor * self.pix_map.size() ) # Slot to receive the cursorMoved signal from the editview widget. If we # are in no_image state, do nothing. If the cursor_to_image switch is # not checked, do nothing. Else get the character position of # the high-end of the current edit selection, and use that to get the # current page index from pagedata, and pass that to _show_page. def cursor_move(self): if self.no_image : return if self.cursor_to_image.isChecked() : pos = self.editor.textCursor().selectionEnd() self._show_page( self.page_data.page_index(pos) ) # Slots to receive the signals from our zoom percent and zoom-to buttons. # The controls are disabled while we are in no_image state, so if a signal # arrives, we are not in that state. # # These are strictly internal hence _names. # Any change in the value of the zoom % spin-box including setValue(). def _new_zoom_pct(self,new_value): self.zoom_factor = self.zoom_pct.value() / 100 self._show_page(self.last_index) # Set a new zoom factor (a real) and update the zoom pct spinbox. # Setting zoom_pct triggers a signal to _new_zoom_pct above, and # thence to _show_page which repaints the page at the new scale value. def _set_zoom_real(self,new_value): zoom = max(new_value, ZOOM_FACTOR_MIN) zoom = min(zoom, ZOOM_FACTOR_MAX) self.zoom_factor = zoom self.zoom_pct.setValue(int(100*zoom)) # Re-implement keyPressEvent in order to provide zoom and page up/down. # ctrl-plus increases the image size by 1.25 # ctrl-minus decreases the image size by 0.8 # page-up displays the next-higher page # page-down displays the next-lower page def keyPressEvent(self, event): # assume we will not handle this key and clear its accepted flag event.ignore() if self.no_image or (self.last_index is None) : return # ignore keys until we are showing some image # We have images to show, check the key value. modkey = int( int(event.key() | (int(event.modifiers()) & C.KEYPAD_MOD_CLEAR)) ) if modkey in C.KEYS_ZOOM : event.accept() fac = (0.8) if (modkey == C.CTL_MINUS) else (1.25) self._set_zoom_real( fac * self.zoom_factor) elif (event.key() == Qt.Key_PageUp) or (event.key() == Qt.Key_PageDown) : event.accept() pgix = self.last_index + (1 if (event.key() == Qt.Key_PageDown) else -1) # If not paging off either end, show that page if pgix >= 0 and pgix < self.page_data.page_count() : self._show_page(pgix) if self.image_to_cursor.isChecked(): self.edit_view.show_position(self.page_data.position(pgix)) # Zoom to width and zoom to height are basically the same thing: # 1. Using the QImage of the current page in self.image, # scan its pixels to find the width (height) of the nonwhite area. # 2. Get the ratio of that to our image label's viewport width (height). # 3. Set that ratio as the zoom factor and redraw the image. # 5. Set the scroll position(s) of our scroll area to left-justify the text. # # We get access to the pixel data using QImage.bits() which gives us a # "sip.voidptr" object that we can index to get byte values. def _zoom_to_width(self): # Generic loop to scan inward from the left or right edge of one # column inward until a dark pixel is seen, returning that margin. def inner_loop(row_range, col_start, margin, col_step): pa, pb = 255, 255 # virtual white outside column for row in row_range: for col in range(col_start, margin, col_step): pc = color_table[ bytes_ptr[row+col] ] if (pa + pb + pc) < 24 : # black or dark gray trio margin = col # new, narrower, margin break # no need to look further on this row pa, pb = pb, pc # else shift 3-pixel window return margin - (2*col_step) # allow for 3-px window if self.no_image or self.image.isNull() : return # nothing to do scale_factor = 4 orig_rows = self.image.height() # number of pixels high orig_cols = self.image.width() # number of logical pixels across # Scale the image to 1/4 size (1/16 the pixel count) and then force # it to indexed-8 format, one byte per pixel. work_image = self.image.scaled( QSize(int(orig_cols/scale_factor),int(orig_rows/scale_factor)), Qt.KeepAspectRatio, Qt.FastTransformation) work_image = work_image.convertToFormat(QImage.Format_Indexed8,Qt.ColorOnly) # Get a reduced version of the color table by extracting just the GG # values of each entry, as a dict keyed by the pixel byte value. For # PNG-2, this gives [0,255] but it could have 8, 16, even 256 elements. color_table = { bytes([c]): int((work_image.color(c) >> 8) & 255) for c in range(work_image.colorCount()) } # Establish limits for the inner loop rows = work_image.height() # number of pixels high cols = work_image.width() # number of logical pixels across stride = (cols + 3) & (-4) # scan-line width in bytes bytes_ptr = work_image.bits() # uchar * a_bunch_o_pixels bytes_ptr.setsize(stride * rows) # make the pointer indexable # Scan in from left and from right to find the outermost dark spots. # Pages tend to start with many lines of white pixels so in hopes of # establishing a narrow margin quickly, scan from the middle to the # end, then do the top half. left_margin = inner_loop( range(int(rows/2)*stride, (rows-1)*stride, stride*2), 0, int(cols/2), 1 ) left_margin = inner_loop( range(0, int(rows/2)*stride, stride*2), 0, left_margin, 1 ) # Now do exactly the same but for the right margin. right_margin = inner_loop( range(int(rows/2)*stride, (rows-1)*stride, stride*2), cols-1, int(cols/2), -1 ) right_margin = inner_loop( range(0, int(rows/2)*stride, stride*2), cols-1, right_margin, -1 ) # Adjust the margins by the scale factor to fit the full size image. #left_margin = max(0,left_margin*scale_factor-scale_factor) #right_margin = min(orig_cols,right_margin*scale_factor+scale_factor) left_margin = left_margin*scale_factor right_margin = right_margin*scale_factor text_size = right_margin - left_margin + 2 port_width = self.scroll_area.viewport().width() # Set the new zoom factor, after limiting by min/max values self._set_zoom_real(port_width/text_size) # Set the scrollbar to show the page from its left margin. self.scroll_area.horizontalScrollBar().setValue( int( left_margin * self.zoom_factor) ) # and that completes zoom-to-width def _zoom_to_height(self): def dark_row(row_start, cols): ''' Scan one row of pixels and return True if it contains at least one 3-pixel blob of darkness, or False if not. ''' pa, pb = 255, 255 for c in range(row_start,row_start+cols): pc = color_table[ bytes_ptr[c] ] if (pa + pb + pc) < 24 : # black or dark gray trio return True pa, pb = pb, pc return False # row was all-white-ish if self.no_image or self.image.isNull() : return # nothing to do scale_factor = 4 orig_rows = self.image.height() # number of pixels high orig_cols = self.image.width() # number of logical pixels across # Scale the image to 1/4 size (1/16 the pixel count) and then force # it to indexed-8 format, one byte per pixel. work_image = self.image.scaled( QSize(int(orig_cols/scale_factor),int(orig_rows/scale_factor)), Qt.KeepAspectRatio, Qt.FastTransformation) work_image = work_image.convertToFormat(QImage.Format_Indexed8,Qt.ColorOnly) # Get a reduced version of the color table by extracting just the GG # values of each entry, as a dict keyed by the pixel byte value. For # PNG-2, this gives [0,255] but it could have 8, 16, even 256 elements. color_table = { bytes([c]): int((work_image.color(c) >> 8) & 255) for c in range(work_image.colorCount()) } rows = work_image.height() # number of pixels high cols = work_image.width() # number of logical pixels across stride = (cols + 3) & (-4) # scan-line width in bytes bytes_ptr = work_image.bits() # uchar * a_bunch_o_pixels bytes_ptr.setsize(stride * rows) # make the pointer indexable # Scan the image rows from the top down looking for one with darkness for top_row in range(rows): if dark_row(top_row*stride, cols): break if top_row > (rows/2) : # too much white, skip it return for bottom_row in range(rows-1, top_row, -1): if dark_row(bottom_row*stride, cols) : break # bottom_row has to be >= top_row. if they are too close together # set_zoom_real will limit the zoom to 200%. top_row = top_row*scale_factor bottom_row = bottom_row*scale_factor text_height = bottom_row - top_row + 1 port_height = self.scroll_area.viewport().height() self._set_zoom_real(port_height/text_height) self.scroll_area.verticalScrollBar().setValue( int( top_row * self.zoom_factor ) ) # and that completes zoom-to-height # Build the widgetary contents. The widget consists mostly of a vertical # layout with two items: A scrollArea containing a QLabel used to display # an image, and a horizontal layout containing the zoom controls. # TODO: figure out design and location of two cursor-link tool buttons. def _uic(self): # Function to return the actual width of the label text # of a widget. Get the fontMetrics and ask it for the width. def _label_width(widget): fm = widget.fontMetrics() return fm.width(widget.text()) # Create a gray field to use when no image is available self.gray_image = QPixmap(700,900) self.gray_image.fill(QColor("gray")) # Build the QLabel that displays the image pixmap. It gets all # available space and scales its contents to fit that space. self.image_display = QLabel() self.image_display.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.image_display.setScaledContents(True) # Create a scroll area within which to display the image. It will # create a horizontal and/or vertical scroll bar when # the image_display size exceeds the size of the scroll area. self.scroll_area = QScrollArea() self.scroll_area.setBackgroundRole(QPalette.Dark) self.scroll_area.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) self.scroll_area.setWidget(self.image_display) # Make sure the scroll area does not swallow user keystrokes self.setFocusPolicy(Qt.ClickFocus) # focus into whole widget self.scroll_area.setFocusProxy(self) # you, pass it on. # Create the image-linking toolbuttons. # Cursor-to-image uses left-hands. c2i_on = QPixmap(':/hand-left-closed.png') c2i_off = QPixmap(':/hand-left-open.png') c2i_con = QIcon() c2i_con.addPixmap(c2i_on,QIcon.Normal,QIcon.On) c2i_con.addPixmap(c2i_off,QIcon.Normal,QIcon.Off) self.cursor_to_image = QToolButton() self.cursor_to_image.setCheckable(True) self.cursor_to_image.setContentsMargins(0,0,0,0) self.cursor_to_image.setIconSize(QSize(30,24)) self.cursor_to_image.setMaximumSize(QSize(32,26)) self.cursor_to_image.setIcon(c2i_con) # Image-to-cursor uses right-hands. i2c_on = QPixmap(':/hand-right-closed.png') i2c_off = QPixmap(':/hand-right-open.png') i2c_con = QIcon() i2c_con.addPixmap(i2c_on,QIcon.Normal,QIcon.On) i2c_con.addPixmap(i2c_off,QIcon.Normal,QIcon.Off) self.image_to_cursor = QToolButton() self.image_to_cursor.setCheckable(True) self.image_to_cursor.setContentsMargins(0,0,0,0) self.image_to_cursor.setIconSize(QSize(30,24)) self.image_to_cursor.setMaximumSize(QSize(32,26)) self.image_to_cursor.setIcon(i2c_con) # Create a spinbox to set the zoom from 15 to 200 and connect its # signal to our slot. self.zoom_pct = QSpinBox() self.zoom_pct.setRange( int(100*ZOOM_FACTOR_MIN),int(100*ZOOM_FACTOR_MAX)) self.zoom_pct.setToolTip( _TR('Imageview zoom control tooltip', 'Set the magnification of the page image') ) # Connect the valueChanged(int) signal as opposed to the # valueChanged(str) signal. self.zoom_pct.valueChanged['int'].connect(self._new_zoom_pct) # Create a label for the zoom spinbox. (the label is not saved as a # class member, its layout will keep it in focus) Not translating # the word "Zoom". pct_label = QLabel( '&Zoom {0}-{1}%'.format( str(self.zoom_pct.minimum() ), str(self.zoom_pct.maximum() ) ) ) pct_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) pct_label.setBuddy(self.zoom_pct) # Create the to-width and to-height zoom buttons. Make # sure their widths are equal after translation. self.zoom_to_width = QPushButton( _TR('Imageview zoom control button name','to Width') ) self.zoom_to_width.setToolTip( _TR('Imageview zoom control tooltip', 'Adjust the image to fill the window side to side.') ) self.zoom_to_width.clicked.connect(self._zoom_to_width) self.zoom_to_height = QPushButton( _TR('Imageview zoom control button name','to Height') ) self.zoom_to_height.setToolTip( _TR('Imageview zoom control tooltip', 'Adjust the image to fill the window top to bottom.') ) self.zoom_to_height.clicked.connect(self._zoom_to_height) w = 20 + max(_label_width(self.zoom_to_height),_label_width(self.zoom_to_width)) self.zoom_to_height.setMinimumWidth(w) self.zoom_to_width.setMinimumWidth(w) # Create an HBox for the top of the panel which contains only # the cursor-to-image link button. tophbox = QHBoxLayout() tophbox.setContentsMargins(0,0,0,0) tophbox.addWidget(self.cursor_to_image,0) tophbox.addStretch() # left-align the button # Create an HBox layout to contain the above controls, using # spacers left and right to center them and a spacers between # to control the spacing. zhbox = QHBoxLayout() zhbox.setContentsMargins(0,0,0,0) zhbox.addWidget(self.image_to_cursor,0) zhbox.addStretch(2) # left and right spacers have stretch 2 zhbox.addWidget(pct_label,0) zhbox.addWidget(self.zoom_pct,0) zhbox.addStretch(1) # spacers between widgets are stretch 1 zhbox.addWidget(self.zoom_to_height,0) zhbox.addSpacing(10) # juuuust a little space between buttons zhbox.addWidget(self.zoom_to_width,0) zhbox.addStretch(2) # right side spacer # With all the pieces in hand, create our layout with a stack of # image over row of controls. vbox = QVBoxLayout() vbox.setContentsMargins(0,0,0,0) vbox.addLayout(tophbox,0) # The image gets a high stretch and default alignment. vbox.addWidget(self.scroll_area,2) vbox.addLayout(zhbox,0) self.setLayout(vbox) # And that completes the UI setup.