Пример #1
0
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)
Пример #2
0
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()
Пример #3
0
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
Пример #4
0
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)
Пример #6
0
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()
Пример #7
0
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()
Пример #8
0
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)
Пример #9
0
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')
Пример #10
0
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()
Пример #11
0
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)
Пример #12
0
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)
Пример #13
0
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)
Пример #14
0
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)
Пример #15
0
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)
Пример #16
0
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