class WorkerSignals(QObject): """Class to define the signals available from a running worker thread. .. note:: signals can only be defined on objects derived from QObject. Supported signals are: finished No data error `tuple` (exctype, value, traceback.format_exc() ) result `object` data returned from processing, anything progress int """ finished = Signal() error = Signal(tuple) result = Signal(str) # Fixme: object progress = Signal(int)
class QmlApplication(QObject): """Class to implement a Qt QML Application.""" _logger = _module_logger.getChild('QmlApplication') ############################################## def __init__(self, application): super().__init__() self._application = application self._scene = None ############################################## sceneChanged = Signal() @Property(QtScene, notify=sceneChanged) def scene(self): return self._scene @scene.setter def scene(self, scene): if self._scene is not scene: self._logger.info('set scene {}'.format(scene)) self._scene = scene self.sceneChanged.emit()
class QmlApplication(QmlBaseApplication.QmlApplication): _logger = _module_logger.getChild('QmlApplication') ############################################## def __init__(self, application): super().__init__(application) self._repository = None ############################################## @Slot(str) def load_repository(self, path): self._logger.info(path) try: self._repository = QmlRepository(path) except NameError as exception: # self.show_message(, warn=True) self._repository = None pass self.repository_changed.emit() ############################################## repository_changed = Signal() @Property(QmlRepository, notify=repository_changed) def repository(self): return self._repository
class QmlApplication(QObject): """Class to implement a Qt QML Application.""" show_message = Signal(str) # message show_error = Signal(str, str) # message backtrace _logger = _module_logger.getChild('QmlApplication') ############################################## def __init__(self, application): super().__init__() self._application = application ############################################## def notify_message(self, message): self.show_message.emit(str(message)) def notify_error(self, message): backtrace_str = traceback.format_exc() self.show_error.emit(str(message), backtrace_str) ############################################## @Property(str, constant=True) def application_name(self): return ApplicationMetadata.name @Property(str, constant=True) def application_url(self): return ApplicationMetadata.url @Property(str, constant=True) def about_message(self): return ApplicationMetadata.about_message()
class QmlBookLibrary(QObject): _logger = _module_logger.getChild('QmlBookLibrary') ############################################## def __init__(self, path): super().__init__() self._book_library = BookLibrary(path) self.scan() ############################################## def _make_book_covers(self): # We must prevent garbage collection self._book_covers = [QmlBookCover(book_cover) for book_cover in self._book_library] @Property(str, constant=True) def path(self): return str(self._book_library.path) ############################################## @Slot() def scan(self): self._book_library.scan() self._make_book_covers() self.books_changed.emit() ############################################## @Slot() def save(self): self._book_library.save_json() ############################################## @Property(str, constant=True) def path(self): return str(self._book_library.path) ############################################## books_changed = Signal() @Property(QQmlListProperty, notify=books_changed) def books(self): return QQmlListProperty(QmlBookCover, self, self._book_covers)
class Shortcut(QObject): _logger = _module_logger.getChild('Shortcut') ############################################## def __init__(self, settings, name, display_name, sequence): super().__init__() self._settings = settings self._name = name self._display_name = display_name self._default_sequence = sequence self._sequence = sequence ############################################## @Property(str, constant=True) def name(self): return self._name @Property(str, constant=True) def display_name(self): return self._display_name @Property(str, constant=True) def default_sequence(self): return self._default_sequence ############################################## sequence_changed = Signal() @Property(str, notify=sequence_changed) def sequence(self): self._logger.info('get sequence {} = {}'.format( self._name, self._sequence)) return self._sequence @sequence.setter def sequence(self, value): if self._sequence != value: self._logger.info('Shortcut {} = {}'.format(self._name, value)) self._sequence = value self._settings.set_shortcut(self) self.sequence_changed.emit()
class QtQuickPaintedSceneItem(QQuickPaintedItem, QtPainter): """Class to implement a painter as Qt Quick item""" _logger = _module_logger.getChild('QtQuickPaintedSceneItem') ############################################## def __init__(self, parent=None): QQuickPaintedItem.__init__(self, parent) QtPainter.__init__(self) # Setup backend rendering self.setAntialiasing(True) # self.setRenderTarget(QQuickPaintedItem.Image) # high quality antialiasing self.setRenderTarget(QQuickPaintedItem.FramebufferObject) # use OpenGL self._viewport_area = ViewportArea() ############################################## def geometryChanged(self, new_geometry, old_geometry): # self._logger.info('geometryChanged', new_geometry, old_geometry) self._viewport_area.viewport_size = new_geometry # if self._scene: # self._update_transformation() QQuickPaintedItem.geometryChanged(self, new_geometry, old_geometry) ############################################## # def _update_transformation(self): # area = self._viewport_area.area # self.translation = - QPointF(area.x.inf, area.y.sup) # self.scale = self._viewport_area.scale_px_by_mm # QtPainter ############################################## @property def scene_area(self): return self._viewport_area.area ############################################## def scene_to_viewport(self, position): return self._viewport_area.scene_to_viewport(position) ############################################## def length_scene_to_viewport(self, length): return self._viewport_area.length_scene_to_viewport(length) ############################################## def length_viewport_to_scene(self, length): return self._viewport_area.length_viewport_to_scene(length) ############################################## sceneChanged = Signal() @Property(QtScene, notify=sceneChanged) def scene(self): return self._scene @scene.setter def scene(self, scene): if self._scene is not scene: # self._logger.info('QtQuickPaintedSceneItem set scene', scene) self._logger.info('set scene') # Fixme: don't print ??? self._scene = scene self._viewport_area.scene = scene self._viewport_area.fit_scene() # self._update_transformation() self.update() self.sceneChanged.emit() ############################################## # zoomChanged = Signal() # @Property(float, notify=zoomChanged) # def zoom(self): # return self._zoom # @zoom.setter # def zoom(self, zoom): # if self._zoom != zoom: # print('QtQuickPaintedSceneItem zoom', zoom, self.width(), self.height()) # self._zoom = zoom # self.set_transformation(zoom) # self.update() # self.zoomChanged.emit() ############################################## @Property(float) def zoom(self): return self._viewport_area.scale_px_by_mm ############################################## @Slot(QPointF, result=str) def format_coordinate(self, position): scene_position = self._viewport_area.viewport_to_scene(position) return '{:.3f}, {:.3f}'.format(scene_position[0], scene_position[1]) ############################################## @Slot(float) def zoom_at_center(self, zoom): self._viewport_area.zoom_at(self._viewport_area.center, zoom) self.update() ############################################## @Slot(QPointF, float) def zoom_at(self, position, zoom): # print('zoom_at', position, zoom) scene_position = self._viewport_area.viewport_to_scene(position) self._viewport_area.zoom_at(scene_position, zoom) self.update() ############################################## @Slot() def fit_scene(self): self._viewport_area.fit_scene() self.update() ############################################## @Slot(QPointF) def pan(self, dxy): position = self._viewport_area.center + self._viewport_area.pan_delta_to_scene( dxy) self._viewport_area.zoom_at(position, self._viewport_area.scale_px_by_mm) self.update() ############################################## @Slot(QPointF) def item_at(self, position, radius_px=10): self._scene.update_rtree() self._scene.unselect_items() scene_position = Vector2D( self._viewport_area.viewport_to_scene(position)) radius = self.length_viewport_to_scene(radius_px) self._logger.info('Item selection at {} with radius {:1f} mm'.format( scene_position, radius)) items = self._scene.item_at(scene_position, radius) if items: distance, nearest_item = items[0] # print('nearest item at {} #{:6.2f} {} {}'.format(scene_position, len(items), distance, nearest_item.user_data)) nearest_item.selected = True # Fixme: z_value ??? for pair in items[1:]: distance, item = pair # print(' {:6.2f} {}'.format(distance, item.user_data)) self.update()
class QmlApplication(QObject): """Class to implement a Qt QML Application.""" show_message = Signal(str) # message show_error = Signal(str, str) # message backtrace scanner_ready = Signal() # Fixme: !!! preview_done = Signal(str) file_exists_error = Signal(str) path_error = Signal(str) scan_done = Signal(str) _logger = _module_logger.getChild('QmlApplication') ############################################## def __init__(self, application): super().__init__() self._application = application ############################################## def notify_message(self, message): self.show_message.emit(str(message)) def notify_error(self, message): backtrace_str = traceback.format_exc() self.show_error.emit(str(message), backtrace_str) ############################################## @Property(str, constant=True) def application_name(self): return ApplicationMetadata.name @Property(str, constant=True) def application_url(self): return ApplicationMetadata.url @Property(str, constant=True) def about_message(self): return ApplicationMetadata.about_message() ############################################## library_changed = Signal() @Property(QmlBookLibrary, notify=library_changed) def library(self): # return null if None return self._application.library ############################################## @Slot('QUrl') def load_library(self, url): path = url.toString(QUrl.RemoveScheme) self._application.load_library(path) self.library_changed.emit() ############################################## book_changed = Signal() @Property(QmlBook, notify=book_changed) def book(self): # return null if None return self._application.book ############################################## @Slot('QUrl') def load_book(self, url): path = url.toString(QUrl.RemoveScheme) self._application.load_book(path) self.book_changed.emit() ############################################## @Slot() def init_scanner(self): def job(): self._application.init_scanner() return '' # Fixme: worker = Worker(job) # worker.signals.result.connect(self.print_output) worker.signals.finished.connect(self.scanner_ready) # worker.signals.progress.connect(self.progress_fn) Application.instance.thread_pool.start(worker) ############################################## @Property(QmlScanner, constant=True) def scanner(self): return self._application.scanner ############################################## @Slot() def debug(self): self._application.scanner.preview_done.connect(self.preview_done) self._application.scanner.file_exists_error.connect( self.file_exists_error) self._application.scanner.path_error.connect(self.path_error) self._application.scanner.scan_done.connect(self._on_scan_done) # self._application.scanner.scan_done.connect(self.scan_done) ############################################## def _on_scan_done(self, path): # Fixme: not received # 13:00,363 - BookBrowser.QtApplication.QmlApplication.Application._message_handler - INFO - ScannerUI.qml on_scanner_ready — Scanner config loaded true # 13:01,110 - BookBrowser.QtApplication.QmlScanner.QmlScanner.scan - INFO - # 13:01,111 - BookBrowser.QtApplication.Runnable.Worker.run - INFO - run <function QmlScanner.scan.<locals>.job at 0x7f4962611e18>((), {}) # 13:01,111 - BookBrowser.Scanner.FakeScanner.scan - INFO - Scan /home/fabrice/home/developpement/python/book-browser/test-directory/afoo.{:03}.png 93 # overwrite = False # 13:01,112 - BookBrowser.Scanner.FakeScanner.scan_image - INFO - Start scanning ... # 13:01,112 - BookBrowser.Scanner.FakeScanner.scan_image - INFO - Start done # 13:01,150 - BookBrowser.Scanner.FakeScanner.scan - INFO - Saved /home/fabrice/home/developpement/python/book-browser/test-directory/afoo.093.png # 13:01,151 - BookBrowser.QtApplication.Runnable.Worker.run - INFO - emit result /home/fabrice/home/developpement/python/book-browser/test-directory/afoo.093.png # 13:01,151 - BookBrowser.QtApplication.Runnable.Worker.run - INFO - emit finished self._logger.info(path) self.scan_done.emit(path)
class Application(QObject): """Class to implement a Qt Application.""" instance = None _logger = _module_logger.getChild('Application') scanner_ready = Signal() ############################################## # Fixme: Singleton @classmethod def create(cls, *args, **kwargs): if cls.instance is not None: raise NameError('Instance exists') cls.instance = cls(*args, **kwargs) return cls.instance ############################################## def __init__(self): self._logger.info('Ctor') super().__init__() QtCore.qInstallMessageHandler(self._message_handler) self._parse_arguments() self._library = None self._book = None # Fixme: must be defined before QML if BookLibrary.is_library(self._args.path): self.load_library(self._args.path) else: self.load_book(self._args.path) # For Qt Labs Platform native widgets # self._application = QGuiApplication(sys.argv) # use QCoreApplication::instance() to get instance self._application = QApplication(sys.argv) self._application.main = self self._init_application() self._engine = QQmlApplicationEngine() self._qml_application = QmlApplication(self) self._application.qml_main = self._qml_application self._platform = QtPlatform() # self._logger.info('\n' + str(self._platform)) self._load_translation() self._register_qml_types() self._set_context_properties() self._load_qml_main() # self._run_before_event_loop() self._thread_pool = QtCore.QThreadPool() self._logger.info("Multithreading with maximum {} threads".format( self._thread_pool.maxThreadCount())) self._scanner = None self._scanner_image_provider = ScannerImageProvider() self._engine.addImageProvider('scanner_image', self._scanner_image_provider) QTimer.singleShot(0, self._post_init) # self._view = QQuickView() # self._view.setResizeMode(QQuickView.SizeRootObjectToView) # self._view.setSource(qml_url) ############################################## @property def args(self): return self._args @property def platform(self): return self._platform @property def settings(self): return self._settings @property def qml_application(self): return self._qml_application @property def thread_pool(self): return self._thread_pool @property def scanner_image_provider(self): return self._scanner_image_provider @property def scanner(self): return self.init_scanner() @property def book(self): return self._book # @property # def book_path(self): # return self._book.path @property def library(self): return self._library ############################################## def _print_critical_message(self, message): # print('\nCritical Error on {}'.format(datetime.datetime.now())) # print('-'*80) # print(message) self._logger.critical(message) ############################################## def _message_handler(self, msg_type, context, msg): if msg_type == QtCore.QtDebugMsg: method = self._logger.debug elif msg_type == QtCore.QtInfoMsg: method = self._logger.info elif msg_type == QtCore.QtWarningMsg: method = self._logger.warning elif msg_type in (QtCore.QtCriticalMsg, QtCore.QtFatalMsg): method = self._logger.critical # method = None # local_msg = msg.toLocal8Bit() # localMsg.constData() context_file = context.file if context_file is not None: file_path = Path(context_file).name else: file_path = '' message = '{1} {3} — {0}'.format(msg, file_path, context.line, context.function) if method is not None: method(message) else: self._print_critical_message(message) ############################################## def _on_critical_exception(self, exception): message = str(exception) + '\n' + traceback.format_exc() self._print_critical_message(message) self._qml_application.notify_error(exception) # sys.exit(1) ############################################## def _init_application(self): self._application.setOrganizationName( ApplicationMetadata.organisation_name) self._application.setOrganizationDomain( ApplicationMetadata.organisation_domain) self._application.setApplicationName(ApplicationMetadata.name) self._application.setApplicationDisplayName( ApplicationMetadata.display_name) self._application.setApplicationVersion(ApplicationMetadata.version) logo_path = ':/icons/logo/logo-256.png' self._application.setWindowIcon(QIcon(logo_path)) QIcon.setThemeName('material') self._settings = ApplicationSettings() ############################################## @classmethod def setup_gui_application(self): # https://bugreports.qt.io/browse/QTBUG-55167 # for path in ( # 'qt.qpa.xcb.xcberror', # ): # QtCore.QLoggingCategory.setFilterRules('{} = false'.format(path)) QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # QQuickStyle.setStyle('Material') ############################################## def _parse_arguments(self): parser = argparse.ArgumentParser(description='BookBrowser', ) # parser.add_argument( # '--version', # action='store_true', default=False, # help="show version and exit", # ) # Fixme: should be able to start application without !!! parser.add_argument( 'path', metavar='PATH', action=PathAction, help='book or library path', ) parser.add_argument( '--dont-translate', action='store_true', default=False, help="Don't translate application", ) parser.add_argument( '--watcher', action='store_true', default=False, help='start watcher', ) parser.add_argument( '--fake-scanner', action='store_true', default=False, help='use a fake scanner', ) parser.add_argument( '--user-script', action=PathAction, default=None, help='user script to execute', ) parser.add_argument( '--user-script-args', default='', help="user script args (don't forget to quote)", ) self._args = parser.parse_args() ############################################## def _load_translation(self): if self._args.dont_translate: return # Fixme: ConfigInstall # directory = ':/translations' directory = str(Path(__file__).parent.joinpath('rcc', 'translations')) locale = QtCore.QLocale() self._translator = QtCore.QTranslator() if self._translator.load(locale, 'book-browser', '.', directory, '.qm'): self._application.installTranslator(self._translator) else: raise NameError('No translator for locale {}'.format( locale.name())) ############################################## def _register_qml_types(self): qmlRegisterType(KeySequenceEditor, 'BookBrowser', 1, 0, 'KeySequenceEditor') qmlRegisterUncreatableType(Shortcut, 'BookBrowser', 1, 0, 'Shortcut', 'Cannot create Shortcut') qmlRegisterUncreatableType(ApplicationSettings, 'BookBrowser', 1, 0, 'ApplicationSettings', 'Cannot create ApplicationSettings') qmlRegisterUncreatableType(QmlApplication, 'BookBrowser', 1, 0, 'QmlApplication', 'Cannot create QmlApplication') qmlRegisterUncreatableType(QmlBookCover, 'BookBrowser', 1, 0, 'QmlBookCover', 'Cannot create QmlBookCover') qmlRegisterUncreatableType(QmlBookLibrary, 'BookBrowser', 1, 0, 'QmlBookLibrary', 'Cannot create QmlBookLi') qmlRegisterUncreatableType(QmlBook, 'BookBrowser', 1, 0, 'QmlBook', 'Cannot create QmlBook') qmlRegisterUncreatableType(QmlBookPage, 'BookBrowser', 1, 0, 'QmlBookPage', 'Cannot create QmlBookPage') qmlRegisterUncreatableType(QmlBookMetadata, 'BookBrowser', 1, 0, 'QmlBookMetadata', 'Cannot create QmlBookMetadata') qmlRegisterUncreatableType(QmlScannerConfig, 'BookBrowser', 1, 0, 'QmlScannerConfig', 'Cannot create QmlScannerConfig') qmlRegisterUncreatableType(QmlScanner, 'BookBrowser', 1, 0, 'QmlScanner', 'Cannot create QmlScanner') ############################################## def _set_context_properties(self): context = self._engine.rootContext() context.setContextProperty('application', self._qml_application) context.setContextProperty('application_settings', self._settings) ############################################## def _load_qml_main(self): self._logger.info('Load QML...') qml_path = Path(__file__).parent.joinpath('qml') # qml_path = 'qrc:///qml' self._engine.addImportPath(str(qml_path)) main_qml_path = qml_path.joinpath('main.qml') self._qml_url = QUrl.fromLocalFile(str(main_qml_path)) # QUrl('qrc:/qml/main.qml') self._engine.objectCreated.connect(self._check_qml_is_loaded) self._engine.load(self._qml_url) self._logger.info('QML loaded') ############################################## def _check_qml_is_loaded(self, obj, url): # See https://bugreports.qt.io/browse/QTBUG-39469 if (obj is None and url == self._qml_url): sys.exit(-1) ############################################## def exec_(self): # self._view.show() self._logger.info('Start event loop') sys.exit(self._application.exec_()) ############################################## def _post_init(self): # Fixme: ui refresh ??? self._logger.info('post Init...') if self._args.watcher: self._logger.info('Start watcher') self._book.start_watcher() # QtCore.QFileSystemWatcher(self) if self._args.user_script is not None: self.execute_user_script(self._args.user_script) self._logger.info('Post Init Done') ############################################## def execute_user_script(self, script_path): """Execute an user script provided by file *script_path* in a context where is defined a variable *application* that is a reference to the application instance. """ script_path = Path(script_path).absolute() self._logger.info('Execute user script:\n {}'.format(script_path)) try: source = open(script_path).read() except FileNotFoundError: self._logger.info('File {} not found'.format(script_path)) sys.exit(1) try: bytecode = compile(source, script_path, 'exec') except SyntaxError as exception: self._on_critical_exception(exception) try: exec(bytecode, {'application': self}) except Exception as exception: self._on_critical_exception(exception) self._logger.info('User script done') ############################################## def init_scanner(self): if self._scanner is None: # take time self._scanner = QmlScanner(fake=self._args.fake_scanner) self.scanner_ready.emit() return self._scanner ############################################## def load_book(self, path): self._logger.info('Load book {} ...'.format(path)) self._book = QmlBook(path) self._logger.info('Book loaded') ############################################## def load_library(self, path): self._logger.info('Load library {} ...'.format(path)) self._library = QmlBookLibrary(path) self._logger.info('library loaded')
class ApplicationSettings(QSettings): """Class to implement application settings.""" _logger = _module_logger.getChild('ApplicationSettings') ############################################## def __init__(self): super().__init__() self._logger.info('Loading settings from {}'.format(self.fileName())) self._shortcut_map = { name: Shortcut(self, name, self._shortcut_display_name(name), self._get_shortcut(name)) for name in self._shortcut_names } self._shortcuts = list(self._shortcut_map.values()) ############################################## @property def _shortcut_names(self): return [name for name in dir(Shortcuts) if not name.startswith('_')] def _shortcut_display_name(self, name): return getattr(Shortcuts, name)[0] def _default_shortcut(self, name): return getattr(Shortcuts, name)[1] ############################################## def _shortcut_path(self, name): return 'shortcut/{}'.format(name) ############################################## def _get_shortcut(self, name): path = self._shortcut_path(name) if self.contains(path): return self.value(path) else: return self._default_shortcut(name) ############################################## def set_shortcut(self, shortcut): path = self._shortcut_path(shortcut.name) self.setValue(path, shortcut.sequence) ############################################## @Property(QQmlListProperty, constant=True) def shortcuts(self): return QQmlListProperty(Shortcut, self, self._shortcuts) ############################################## @Slot(str, result=Shortcut) def shortcut(self, name): return self._shortcut_map.get(name, None) ############################################## # @Slot(str, result=str) # def shortcut_sequence(self, name): # shortcut = self._shortcut_map.get(name, None) # if shortcut is not None: # return shortcut.sequence # else: # return None ############################################## external_program_changed = Signal() __EXTERNAL_PROGRAM_ID__ = 'external_program' @Property(str, constant=True) def default_external_program(self): # return QUrl('file://' + ) return str(DefaultSettings.ExternalProgram.default) @Property(str, notify=external_program_changed) def external_program(self): if self.contains(self.__EXTERNAL_PROGRAM_ID__): return self.value(self.__EXTERNAL_PROGRAM_ID__) else: return self.default_external_program @external_program.setter def external_program(self, value): self._logger.info('set external sequence {}'.format(value)) if value != self.external_program: self._logger.info('set external sequence {}'.format(value)) self.setValue(self.__EXTERNAL_PROGRAM_ID__, value) self.external_program_changed.emit()
class QmlRepository(QObject): _logger = _module_logger.getChild('QmlRepository') ############################################## def __init__(self, path=None): super().__init__() self._logger.info('Init Repository') if path is None: path = os.getcwd() try: self._repository = GitRepository(path) except RepositoryNotFound: raise NameError( "Any Git repository was found in path {}".format(path)) self._repository = None return self._branches = [] self._update_branches() self._commit_pool = QmlCommitPool(self) self._commits = [] self._update_commits() self._tags = [] self._update_tags() ############################################## def __bool__(self): return self._repository is not None ############################################## @Property(QmlCommitPool, constant=True) def commit_pool(self): return self._commit_pool ############################################## def _update_branches(self): self._branches = [ QmlBranch(self, branch) for branch in self._repository.branches ] self.branches_changed.emit() branches_changed = Signal() @Property(QQmlListProperty, notify=branches_changed) def branches(self): return QQmlListProperty(QmlBranch, self, self._branches) ############################################## def _update_commits(self): self._commits = [ self._commit_pool.from_commit(commit) for commit in self._repository.commits_for_head ] self.commits_changed.emit() commits_changed = Signal() @Property(QQmlListProperty, notify=commits_changed) def commits(self): return QQmlListProperty(QmlCommit, self, self._commits) ############################################## def _update_tags(self): self._tags = [ QmlReference(self, reference) for reference in self._repository.tags ] self.tags_changed.emit() tags_changed = Signal() @Property(QQmlListProperty, notify=tags_changed) def tags(self): return QQmlListProperty(QmlReference, self, self._tags)
class QmlBookCover(QObject): _logger = _module_logger.getChild('QmlBookCover') ############################################## def __init__(self, book_cover): super().__init__() self._book_cover = book_cover ############################################## @Property(str, constant=True) def path(self): return str(self._book_cover.path) @Property(str, constant=True) def cover_path(self): cover_path = self._book_cover.cover_path if cover_path: return str(cover_path) else: return '' ############################################## # Fixme: duplicate code @Property(int, constant=True) def large_thumbnail_size(self): return FreeDesktopThumbnailCache.LARGE_SIZE large_thumbnail_path_changed = Signal() @Property(str, notify=large_thumbnail_path_changed) def large_thumbnail_path(self): # Fixme: cache thumbnail instance ? cover_path = self.cover_path if cover_path: return str(thumbnail_cache[cover_path].large_path) else: return '' ############################################## thumbnail_ready = Signal() @Slot() def request_large_thumbnail(self): cover_path = self.cover_path if not cover_path: return def job(): # Fixme: issue when the application is closed return str(thumbnail_cache[cover_path].large) worker = Worker(job) # worker.signals.result.connect(self.print_output) worker.signals.finished.connect(self.thumbnail_ready) # worker.signals.progress.connect(self.progress_fn) from .QmlApplication import Application Application.instance.thread_pool.start(worker)
class QmlBookMetadata(QObject): _logger = _module_logger.getChild('QmlBookMetadata') ############################################## def __init__(self, book): super().__init__() self._book = book self._metadata = book.metadata self._dirty = False ############################################## @staticmethod def _to_list(value): return [x.strip() for x in value.split(',')] ############################################## @Property(str, constant=True) def path(self): return self._metadata.path_str ############################################## dirty_changed = Signal() @Property(bool, notify=dirty_changed) def dirty(self): return self._dirty def _set_dirty(self, value=True): if self._dirty != value: self._dirty = value self.dirty_changed.emit() ############################################## authors_changed = Signal() @Property(str, notify=authors_changed) def authors(self): return self._metadata.authors_str @authors.setter def authors(self, value): value = self._to_list(value) if self.authors != value: self._metadata.authors = value self.authors_changed.emit() self._set_dirty() ############################################## isbn_changed = Signal() @Property(str, notify=isbn_changed) def isbn(self): return self._metadata.isbn_str @isbn.setter def isbn(self, value): if self.isbn != value: self._metadata.isbn = value self.isbn_changed.emit() self._set_dirty() ############################################## language_changed = Signal() @Property(str, notify=language_changed) def language(self): return self._metadata.language @language.setter def language(self, value): if self.language != value: self._metadata.language = value self.language_changed.emit() self._set_dirty() ############################################## number_of_pages_changed = Signal() @Property(int, notify=number_of_pages_changed) def number_of_pages(self): return self._metadata.number_of_pages @number_of_pages.setter def number_of_pages(self, value): if self.number_of_pages != value: self._metadata.number_of_pages = value self.number_of_pages_changed.emit() self._set_dirty() ############################################## publisher_changed = Signal() @Property(str, notify=publisher_changed) def publisher(self): return self._metadata.publisher @publisher.setter def publisher(self, value): if self.publisher != value: self._metadata.publisher = value self.publisher_changed.emit() self._set_dirty() ############################################## title_changed = Signal() @Property(str, notify=title_changed) def title(self): return self._metadata.title @title.setter def title(self, value): if self.title != value: self._metadata.title = value self.title_changed.emit() self._set_dirty() ############################################## year_changed = Signal() @Property(int, notify=year_changed) def year(self): return self._metadata.year @year.setter def year(self, value): if self.year != value: self._metadata.year = value self.year_changed.emit() self._set_dirty() ############################################## page_offset_changed = Signal() @Property(int, notify=page_offset_changed) def page_offset(self): return self._metadata.page_offset @page_offset.setter def page_offset(self, value): if self.page_offset != value: self._metadata.page_offset = value self.page_offset_changed.emit() self._set_dirty() ############################################## keywords_changed = Signal() @Property(str, notify=keywords_changed) def keywords(self): return self._metadata.keywords_str @keywords.setter def keywords(self, value): value = self._to_list(value) if self.keywords != value: self._metadata.keywords = value self.keywords_changed.emit() self._set_dirty() ############################################## description_changed = Signal() @Property(str, notify=description_changed) def description(self): return self._metadata.description @description.setter def description(self, value): if self.description != value: self._metadata.description = value self.description_changed.emit() self._set_dirty() ############################################## notes_changed = Signal() notes_html_changed = Signal() @Property(str, notify=notes_changed) def notes(self): return self._metadata.notes @Property(str, notify=notes_html_changed) def notes_html(self): return markdown.markdown(self._metadata.notes) @notes.setter def notes(self, value): if self.notes != value: self._metadata.notes = value self.notes_changed.emit() self.notes_html_changed.emit() self._set_dirty() ############################################## @Slot() def update_from_isbn(self): # Fixme: run in a thread ??? self._metadata.update_from_isbn() self.authors_changed.emit() self.language_changed.emit() self.publisher_changed.emit() self.title_changed.emit() self.year_changed.emit() self._set_dirty() ############################################## @Slot() def save(self): self._book.save_metadata() self._set_dirty(False)
class QmlBook(QObject): new_page = Signal(int) _logger = _module_logger.getChild('QmlBook') ############################################## def __init__(self, path): super().__init__() self._book = Book(path) self._book.fix_empty_pages() self._metadata = QmlBookMetadata(self._book) # We must prevent garbage collection self._pages = [QmlBookPage(self, page) for page in self._book] ############################################## @Property(str, constant=True) def path(self): return str(self._book.path) ############################################## @Property(QmlBookMetadata, constant=True) def metadata(self): return self._metadata ############################################## number_of_pages_changed = Signal() @Property(int, notify=number_of_pages_changed) def number_of_pages(self): # return self._book.number_of_pages return len(self._pages) @Slot(int, result=bool) def is_valid_page_number(self, page_number): return 0 < page_number <= self.number_of_pages ############################################## last_page_number_changed = Signal() @Property(int, notify=last_page_number_changed) def last_page_number(self): return self._book.last_page_number ############################################## pages_changed = Signal() @Property(QQmlListProperty, notify=pages_changed) def pages(self): return QQmlListProperty(QmlBookPage, self, self._pages) ############################################## @Property(QmlBookPage) def first_page(self): try: return self._pages[0] except IndexError: return None @Property(QmlBookPage) def last_page(self): try: return self._pages[-1] except IndexError: return None @Slot(int, result=QmlBookPage) def page(self, page_number): try: return self._pages[page_number - 1] except IndexError: return None ############################################## @Slot(QmlBookPage, str) def flip_from_page(self, qml_page, orientation): # Fixme: qml_page.page.page_number is None self._logger.info('{} {}'.format(qml_page.page_number, orientation)) self._book.flip_from_page(qml_page.page_number, orientation) qml_page.orientation_changed.emit() self.pages_changed.emit() ############################################## def start_watcher(self, watcher=None): self._files = set(self._glob_files()) self._watcher = watcher or QFileSystemWatcher() self._watcher.addPath(str(self._book.path)) self._watcher.directory_changed.connect(self._on_directory_change) ############################################## def _glob_files(self): pattern = str(self._book.path.joinpath('*' + self._book.extension)) for path in glob.glob(pattern): yield Path(path).name ############################################## def _on_directory_change(self, path): time.sleep(3) # QTimer::singleShot(200, this, SLOT(updateCaption())); files = set(self._glob_files()) new_files = files - self._files self._logger.info('New files {}'.format(new_files)) # Fixme: overwrite for filename in new_files: self._on_new_file(filename) self._files = files ############################################## def _on_new_file(self, filename): self._logger.info('New file {}'.format(filename)) # try: page = self._book.add_page(filename) page._page_number = self._book.number_of_pages # Fixme: !!! self._logger.info('New page\n{}'.format(page)) self._pages.append(QmlBookPage(self, page)) self.number_of_pages_changed.emit() self.new_page.emit(page.page_number)
class QmlBookPage(QObject): _logger = _module_logger.getChild('QmlBookPage') ############################################## def __init__(self, qml_book, book_page): super().__init__() self._qml_book = qml_book self._page = book_page self._text = None self._ocr_running = False self.text_ready.connect(self._ocr_cleanup) ############################################## def __repr__(self): return '{0} {1}'.format(self.__class__.__name__, self._page) ############################################## @property def page(self): return self._page ############################################## @Property(bool, constant=True) def is_empty(self): return self._page.is_empty ############################################## path_changed = Signal() @Property(str, notify=path_changed) def path(self): if self._page.is_empty: return '' else: return str(self._page.path) ############################################## @Property(int, constant=True) def large_thumbnail_size(self): return FreeDesktopThumbnailCache.LARGE_SIZE large_thumbnail_path_changed = Signal() @Property(str, notify=large_thumbnail_path_changed) def large_thumbnail_path(self): # Fixme: cache thumbnail instance ? return str(thumbnail_cache[self._page.path].large_path) ############################################## thumbnail_ready = Signal() @Slot() def request_large_thumbnail(self): def job(): # Fixme: issue when the application is closed return str(thumbnail_cache[self._page.path].large) worker = Worker(job) worker.signals.finished.connect(self.thumbnail_ready) from .QmlApplication import Application Application.instance.thread_pool.start(worker) ############################################## page_number_changed = Signal() @Property(int, notify=page_number_changed) def page_number(self): return int(self._page) # self._page.page_number ############################################## orientation_changed = Signal() @Property(int, notify=orientation_changed) def orientation(self): return 180 if self._page.orientation == 'v' else 0 ############################################## @Slot() def flip_page(self): self._page.flip() self.orientation_changed.emit() self._qml_book.pages_changed.emit() ############################################## text_ready = Signal() @Property(str, constant=True) def text(self): if not self._ocr_running and self._text is None: metadata = self._page.book.metadata language = metadata.language or None def job(): # Set fake to debug and receive a large lorem ipsum text = self._page.to_text(language, fake=False) # use result signal ??? self._text = text # return text worker = Worker(job) worker.signals.finished.connect(self.text_ready) self._ocr_running = True from .QmlApplication import Application Application.instance.thread_pool.start(worker) return self._text def _ocr_cleanup(self): self._ocr_running = False ############################################## @Slot(QUrl) def save_text(self, url): path = url.toString(QUrl.RemoveScheme) try: with open(path, 'w') as fh: fh.write(self.text) self._logger.info('Save text page in {}'.format(path)) except: application = QCoreApplication.instance() qml_application = application.qml_main tr_str = QCoreApplication.translate('QmlBookPage', 'Could not save file {}') qml_application.notify_message(tr_str.format(path)) ############################################## @Slot(str) def open_in_external_program(self, program): command = (program, self.path) self._logger.info(' '.join(command)) process = subprocess.Popen(command)
class KeySequenceEditor(QQuickItem): _logger = _module_logger.getChild('KeySequenceEditor') ############################################## def __init__(self, parent): super().__init__(parent) self._default_sequence = QKeySequence() # default sequence self._edited_sequence = QKeySequence() # current/edited sequence self._new_sequence = QKeySequence() # customised sequence self._reset_pressed_keys() ############################################## def _reset_pressed_keys(self): self._logger.info('Clearing pressed keys') self._pressed_keys = [] ############################################## default_sequence_changed = Signal() @Property(str, notify=default_sequence_changed) def default_sequence(self): return self._default_sequence.toString() ############################################## @default_sequence.setter def default_sequence(self, default_sequence): if default_sequence != self._default_sequence.toString(): self._default_sequence = QKeySequence(default_sequence, QKeySequence.PortableText) self._set_edited_sequence('') # Fixme: why reset ??? self.new_sequence = '' self.default_sequence_changed.emit() # This might not always be the case, I'm just lazy. self.is_customised_changed.emit() self.display_sequence_changed.emit() ############################################## new_sequence_changed = Signal() @Property(str, notify=new_sequence_changed) def new_sequence(self): return self._new_sequence.toString() @new_sequence.setter def new_sequence(self, new_sequence): if new_sequence != self._new_sequence.toString(): self._new_sequence = QKeySequence(new_sequence, QKeySequence.PortableText) self._logger.info('Set new sequence to {}'.format( self._new_sequence.toString())) self.new_sequence_changed.emit() self.is_customised_changed.emit() self.display_sequence_changed.emit() ############################################## display_sequence_changed = Signal() @Property(str, notify=display_sequence_changed) def display_sequence(self): """Text to show in the sequence editor""" if self.hasActiveFocus(): # we are editing the sequence sequence = self._edited_sequence elif self._new_sequence.isEmpty(): # no new sequence sequence = self._default_sequence else: sequence = self._new_sequence return sequence.toString() ############################################## is_customised_changed = Signal() @Property(bool, notify=is_customised_changed) def is_customised(self): """Flag to indicate a new valid sequence is set""" return not self._new_sequence.isEmpty( ) and self._new_sequence != self._default_sequence ############################################## @Slot() def reset(self): """Reset the sequence to the default one""" self._set_edited_sequence(self.default_sequence) self.new_sequence = self.default_sequence self._reset_pressed_keys() ############################################## def _set_edited_sequence(self, edited_sequence=''): """Update the edited sequence. emit *is_customised* and *display_sequence* """ if edited_sequence != self._edited_sequence.toString(): self._edited_sequence = QKeySequence(edited_sequence, QKeySequence.PortableText) self._logger.info('Edited sequence changed to {}'.format( self._edited_sequence.toString())) self.is_customised_changed.emit() self.display_sequence_changed.emit() ############################################## def keyPressEvent(self, event): """Handler when a key is pressed. * use Escape to leave the control * use Enter to accept the sequence """ if event.key() == Qt.Key_Escape: self.setFocus(False) elif event.key() == Qt.Key_Return: self._accept() elif not event.isAutoRepeat(): modifiers = 0 # event.modifiers().testFlag(...) if event.modifiers() & Qt.ControlModifier: modifiers |= Qt.CTRL if event.modifiers() & Qt.ShiftModifier: modifiers |= Qt.SHIFT if event.modifiers() & Qt.AltModifier: modifiers |= Qt.ALT if event.modifiers() & Qt.MetaModifier: modifiers |= Qt.META if Qt.Key_Shift <= event.key() <= Qt.Key_Meta: self._logger.info( 'Only modifiers were pressed ({} / {} / {} ignoring'. format(event.text(), _key_name(event.key()), QKeySequence(event.key()).toString())) else: self._pressed_keys.append(event.key() | modifiers) self._logger.info( 'Adding key {} / {} / {} with modifiers ({}) to pressed keys' .format( event.text(), _key_name(event.key()), QKeySequence(event.key()).toString(), # UnicodeEncodeError: 'utf-8' codec can't encode character '\udc21' in position 188: surrogates not allowed QKeySequence(self._pressed_keys[-1]).toString(), )) sequence = QKeySequence(*self._pressed_keys) # up to 4 keys self._set_edited_sequence(sequence.toString()) if len(self._pressed_keys) == 4: # That was the last key out of four possible keys end it here. self._accept() event.accept() ############################################## def keyReleaseEvent(self, event): event.accept() ############################################## def focusInEvent(self, event): event.accept() # The text displaying the shortcut should be cleared when editing begins. self.display_sequence_changed.emit() ############################################## def focusOutEvent(self, event): event.accept() self._cancel() ############################################## def _accept(self): """Update *new_sequence* if the input is valid.""" self._logger.info('Attempting to accept input...') # If there hasn't been anything new successfully entered yet, check against the original # sequence, otherwise check against the latest successfully entered sequence. # Note: is_customised() assumes that an empty sequence isn't possible we might want to account # for this in the future. if ((self._edited_sequence != self._default_sequence) or (self.is_customised and self._edited_sequence != self._new_sequence)): if self._validate(self._edited_sequence): self._logger.info('Input valid') self.new_sequence = self._edited_sequence.toString() else: self._logger.info('Input invalid') self._cancel() else: self._logger.info('Nothing has changed in the input') # Nothing's changed. self._reset_pressed_keys() self.setFocus(False) ############################################## def _cancel(self): self._reset_pressed_keys() if self._edited_sequence.isEmpty(): # If the edited sequence is empty, setting it to an empty string # obviously won't change anything, and it will return early. # We need the display sequence to update though, so call it here. self.display_sequence_changed.emit() else: self._set_edited_sequence('') ############################################## def _validate(self, sequence): """Method to validate the new sequence""" self._logger.info('Validating key sequence {} ...'.format( sequence.toString())) valid = True # False # do some checks return valid