示例#1
0
    def __init__(self, QWidget_parent=None):
        QWidget.__init__(self, QWidget_parent)

        # параметры
        self._Margin = 50
        self._isWatcher = False
        self._isChangeIndex = False
        self._is1Day = False
        self._isMore1Day = False
        self._date = QDate()
        self._curves = {}
        self._legend = ""
        self._plotterScale = PlotterScale(self)

        # параметры, которые сохраняются
        self._colorExt = QColor(Qt.white)
        self._colorInside = QColor(Qt.white)
        self._colorCurve = QColor(Qt.red)
        self._fontGrid = QFont('Arial', 8, QFont.Normal)
        self._fontTitle = QFont('Arial', 20, QFont.Normal)
        self._fontLegend = QFont('Arial', 8, QFont.Normal)
        self._fontTitleY = QFont('Arial', 16, QFont.Normal)
        self._pixmapSaveWidth = 800
        self._pixmapSaveHeight = 600

        self._toolbar = QToolBar(self)

        self._contexMenu = QMenu(self)
        saveAction = self._contexMenu.addAction(self.tr('Сохранить...'))
        saveAction.triggered.connect(self.onSavePlotter)

        self._timer = QTimer(self)
        self._timer.setTimerType(Qt.VeryCoarseTimer)
        self._timer.timeout.connect(self.tickTimer)

        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        self.timerTick = pyqtSignal(name='timerTick')
        self.noWatching = pyqtSignal(bool, name='noWatching')
        self.variablesPlotter = pyqtSignal(QDate, int, name='variablesPlotter')
示例#2
0
class Scene(QObject):
    object_spawned = pyqtSignal(SceneObject, name='object_spawned')
    object_despawned = pyqtSignal(SceneObject, name='object_despawned')

    def __init__(self, width, height):
        super(Scene, self).__init__()
        self.rootNode = SceneNode()
        self.backgroundPixmap = None
        self.backgroundBrush = QBrush(QColor(0, 0, 0))
        self.width = width
        self.height = height
        self.sceneAabb = QRectF(0.0, 0.0, width, height)
        return

    def get_width(self):
        return self.width

    def get_height(self):
        return self.height

    def get_root_node(self):
        return self.rootNode

    def get_background_brush(self):
        return self.backgroundBrush

    def set_background_brush(self, brush):
        self.backgroundBrush = brush

    def get_background_pixmap(self):
        return self.backgroundPixmap

    def set_background_pixmap(self, pixmap):
        self.backgroundPixmap = pixmap

    def draw(self, painter):
        painter.setClipRect(self.sceneAabb)
        self.update_node(self.rootNode)
        if self.backgroundPixmap is not None:
            painter.drawPixmap(self.sceneAabb, self.backgroundPixmap,
                               self.backgroundPixmap.rect())
        else:
            painter.fillRect(self.sceneAabb, self.backgroundBrush)

        self.draw_node(painter, self.rootNode)
        return

    def draw_node(self, painter, node):
        for childNode in node.children:
            if QRectF.intersects(self.sceneAabb, childNode.aabb):
                self.draw_node(painter, childNode)
        node.update_transform()
        if node.attachedObject is not None:
            node.attachedObject.draw(painter)
        return

    def update_node(self, node):
        for childNode in node.children:
            self.update_node(childNode)
        node.update_transform()
        return

    def spawn(self, scene_object, parent_object=None):
        node = SceneNode()
        if parent_object is None:
            self.rootNode.attach_child(node)
        else:
            parent_object.get_scene_node().attach_child(node)
        scene_object.spawn(node)

        self.object_spawned.emit(scene_object)
        return

    def despawn(self, scene_object):
        node = scene_object.get_scene_node()
        if node is not None:
            for child in node.children:
                if child.attachedObject is not None:
                    self.despawn(child.attachedObject)
            scene_object.despawn()
            node.change_parent(None)
            self.object_despawned.emit(scene_object)
        return

    def reset_scene(self):
        self.reset_node(self.rootNode)
        return

    def reset_node(self, node):
        for childNode in reversed(node.children):
            self.reset_node(childNode)
        if node.attachedObject is not None:
            self.despawn(node.attachedObject)
        return
示例#3
0
class BookInfo(HTMLDisplay):

    link_clicked = pyqtSignal(object)
    remove_format = pyqtSignal(int, object)
    remove_item = pyqtSignal(int, object, object)
    save_format = pyqtSignal(int, object)
    restore_format = pyqtSignal(int, object)
    compare_format = pyqtSignal(int, object)
    set_cover_format = pyqtSignal(int, object)
    copy_link = pyqtSignal(object)
    manage_category = pyqtSignal(object, object)
    open_fmt_with = pyqtSignal(int, object, object)
    edit_book = pyqtSignal(int, object)
    edit_identifiers = pyqtSignal()
    find_in_tag_browser = pyqtSignal(object, object)

    def __init__(self, vertical, parent=None):
        HTMLDisplay.__init__(self, parent)
        self.vertical = vertical
        self.anchor_clicked.connect(self.link_activated)
        for x, icon in [
            ('remove_format', 'trash.png'), ('save_format', 'save.png'),
            ('restore_format', 'edit-undo.png'), ('copy_link','edit-copy.png'),
            ('compare_format', 'diff.png'),
            ('set_cover_format', 'default_cover.png'),
            ('find_in_tag_browser', 'search.png')
        ]:
            ac = QAction(QIcon(I(icon)), '', self)
            ac.current_fmt = None
            ac.current_url = None
            ac.triggered.connect(getattr(self, '%s_triggerred'%x))
            setattr(self, '%s_action'%x, ac)
        self.manage_action = QAction(self)
        self.manage_action.current_fmt = self.manage_action.current_url = None
        self.manage_action.triggered.connect(self.manage_action_triggered)
        self.edit_identifiers_action = QAction(QIcon(I('identifiers.png')), _('Edit identifiers for this book'), self)
        self.edit_identifiers_action.triggered.connect(self.edit_identifiers)
        self.remove_item_action = ac = QAction(QIcon(I('minus.png')), '...', self)
        ac.data = (None, None, None)
        ac.triggered.connect(self.remove_item_triggered)
        self.setFocusPolicy(Qt.NoFocus)
        self.setDefaultStyleSheet(css())

    def refresh_css(self):
        self.setDefaultStyleSheet(css(True))

    def remove_item_triggered(self):
        field, value, book_id = self.remove_item_action.data
        if field:
            self.remove_item.emit(book_id, field, value)

    def context_action_triggered(self, which):
        f = getattr(self, '%s_action'%which).current_fmt
        url = getattr(self, '%s_action'%which).current_url
        if f and 'format' in which:
            book_id, fmt = f
            getattr(self, which).emit(book_id, fmt)
        if url and 'link' in which:
            getattr(self, which).emit(url)

    def remove_format_triggerred(self):
        self.context_action_triggered('remove_format')

    def save_format_triggerred(self):
        self.context_action_triggered('save_format')

    def restore_format_triggerred(self):
        self.context_action_triggered('restore_format')

    def compare_format_triggerred(self):
        self.context_action_triggered('compare_format')

    def set_cover_format_triggerred(self):
        self.context_action_triggered('set_cover_format')

    def copy_link_triggerred(self):
        self.context_action_triggered('copy_link')

    def find_in_tag_browser_triggerred(self):
        if self.find_in_tag_browser_action.current_fmt:
            self.find_in_tag_browser.emit(*self.find_in_tag_browser_action.current_fmt)

    def manage_action_triggered(self):
        if self.manage_action.current_fmt:
            self.manage_category.emit(*self.manage_action.current_fmt)

    def link_activated(self, link):
        if unicode_type(link.scheme()) in ('http', 'https'):
            return safe_open_url(link)
        link = unicode_type(link.toString(NO_URL_FORMATTING))
        self.link_clicked.emit(link)

    def show_data(self, mi):
        html = render_html(mi, self.vertical, self.parent())
        set_html(mi, html, self)

    def mouseDoubleClickEvent(self, ev):
        v = self.viewport()
        if v.rect().contains(self.mapFromGlobal(ev.globalPos())):
            ev.ignore()
        else:
            return HTMLDisplay.mouseDoubleClickEvent(self, ev)

    def contextMenuEvent(self, ev):
        details_context_menu_event(self, ev, self, True)

    def open_with(self, book_id, fmt, entry):
        self.open_fmt_with.emit(book_id, fmt, entry)

    def choose_open_with(self, book_id, fmt):
        from calibre.gui2.open_with import choose_program
        entry = choose_program(fmt, self)
        if entry is not None:
            self.open_with(book_id, fmt, entry)

    def edit_fmt(self, book_id, fmt):
        self.edit_book.emit(book_id, fmt)
示例#4
0
class Plugins(QObject):
    class Plugin:
        # enum Type
        Invalid = 0
        InternalPlugin = 1
        SharedLibraryPlugin = 2
        PythonPlugin = 3
        QmlPlugin = 4

        def __init__(self):
            self.type = self.Invalid
            self.pluginId = ''
            self.pluginSpec = PluginSpec()
            self.instance = None  # PluginInterface

            # InternalPlugin
            self.internalInstance = None

            # SharedLibraryPlugin
            self.libraryPath = ''
            self.pluginLoader = None  # QPluginLoader

            # Other
            self.data = None

        def isLoaded(self):
            return not not self.instance

        def __eq__(self, other):
            return (self.type == other.type
                    and self.pluginId == other.pluginId)

    def __init__(self, parent=None):
        super().__init__(parent)
        # protected:
        self._loadedPlugins = []  # QList<PluginInterface>

        # private:
        self._availablePlugins = []  # QList<Plugin>
        self._allowedPlugins = []  # QStringList

        self._pluginsLoaded = False

        self._speedDial = SpeedDial(self)  # SpeedDial
        self._internalPlugins = []  # QList<PluginInterface>
        self._pythonPlugin = None  # QLibrary

        self.loadSettings()
        from mc.app.MainApplication import MainApplication
        if not MainApplication.isTestModeEnabled():
            return self._loadPythonSupport()

    def getAvailablePlugins(self):
        '''
        @return: QList<Plugin>
        '''
        pass

    def loadPlugin(self, plugin):
        '''
        @param: plugin Plugin
        @return: bool
        '''
        pass

    def unloadPlugin(self, plugin):
        '''
        @param: plugin Plugin
        '''
        pass

    def removePlugin(self, plugin):
        '''
        @param: plugin Plugin
        '''
        pass

    def shutdown(self):
        pass

    # SpeedDial
    def speedDial(self):
        '''
        @return: SpeedDial
        '''
        return self._speedDial

    @classmethod
    def createSpec(cls, metaData):
        '''
        @param: metaData DesktopFile
        @return: PluginSpec
        '''
        pass

    # public Q_SLOTS
    def loadSettings(self):
        pass

    def loadPlugins(self):
        pass

    # Q_SIGNALS
    pluginUnloaded = pyqtSignal(PluginInterface)  # plugin
    refreshedLoadedPlugins = pyqtSignal()

    # private:
    def _loadPythonSupport(self):
        pass

    def _loadPlugin(self, id_):
        pass

    def _loadInternalPlugin(self, name):
        pass

    def _loadSharedLibraryPlugin(self, name):
        pass

    def _loadPythonPlugin(self, name):
        pass

    def _initPlugin(self, state, plugin):
        '''
        @param: state PluginInterface::InitState
        @param: plugin Plugin
        @return: bool
        '''
        pass

    def _initInternalPlugin(self, plugin):
        '''
        @param: plugin Plugin
        '''
        pass

    def _initSharedLibraryPlugin(self, plugin):
        '''
        @param: plugin Plugin
        '''
        pass

    def _initPythonPlugin(self, plugin):
        '''
        @param: plugin Plugin
        '''
        pass

    def _registerAvailablePlugin(self, plugin):
        '''
        @param: plugin Plugin
        '''
        pass

    def _refreshLoadedPlugins(self):
        pass

    def _loadAvailablePlugins(self):
        pass
示例#5
0
文件: main.py 项目: kba/calibre
class Preferences(QMainWindow):

    run_wizard_requested = pyqtSignal()

    def __init__(self, gui, initial_plugin=None, close_after_initial=False):
        QMainWindow.__init__(self, gui)
        self.gui = gui
        self.must_restart = False
        self.committed = False
        self.close_after_initial = close_after_initial

        self.resize(930, 720)
        nh, nw = min_available_height() - 25, available_width() - 10
        if nh < 0:
            nh = 800
        if nw < 0:
            nw = 600
        nh = min(self.height(), nh)
        nw = min(self.width(), nw)
        self.resize(nw, nh)
        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
        self.esc_action.triggered.connect(self.esc)

        geom = gprefs.get('preferences_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(geom)

        # Center
        if islinux:
            self.move(gui.rect().center() - self.rect().center())

        self.setWindowModality(Qt.WindowModal)
        self.setWindowTitle(__appname__ + ' - ' + _('Preferences'))
        self.setWindowIcon(QIcon(I('config.png')))

        self.status_bar = StatusBar(self)
        self.setStatusBar(self.status_bar)

        self.stack = QStackedWidget(self)
        self.cw = QWidget(self)
        self.cw.setLayout(QVBoxLayout())
        self.cw.layout().addWidget(self.stack)
        self.bb = QDialogButtonBox(QDialogButtonBox.Close)
        self.wizard_button = self.bb.addButton(_('Run welcome wizard'),
                                               self.bb.ActionRole)
        self.wizard_button.setIcon(QIcon(I('wizard.png')))
        self.wizard_button.clicked.connect(self.run_wizard,
                                           type=Qt.QueuedConnection)
        self.cw.layout().addWidget(self.bb)
        self.bb.button(self.bb.Close).setDefault(True)
        self.bb.rejected.connect(self.close, type=Qt.QueuedConnection)
        self.setCentralWidget(self.cw)
        self.browser = Browser(self)
        self.browser.show_plugin.connect(self.show_plugin)
        self.stack.addWidget(self.browser)
        self.scroll_area = QScrollArea(self)
        self.stack.addWidget(self.scroll_area)
        self.scroll_area.setWidgetResizable(True)

        self.setContextMenuPolicy(Qt.NoContextMenu)
        self.bar = QToolBar(self)
        self.addToolBar(self.bar)
        self.bar.setVisible(False)
        self.bar.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
        self.bar.setMovable(False)
        self.bar.setFloatable(False)
        self.bar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.apply_action = self.bar.addAction(QIcon(I('ok.png')), _('&Apply'),
                                               self.commit)
        self.cancel_action = self.bar.addAction(QIcon(I('window-close.png')),
                                                _('&Cancel'), self.cancel)
        self.bar_title = BarTitle(self.bar)
        self.bar.addWidget(self.bar_title)
        self.restore_action = self.bar.addAction(QIcon(I('clear_left.png')),
                                                 _('Restore &defaults'),
                                                 self.restore_defaults)
        for ac, tt in [('apply', _('Save changes')),
                       ('cancel', _('Cancel and return to overview'))]:
            ac = getattr(self, ac + '_action')
            ac.setToolTip(tt)
            ac.setWhatsThis(tt)
            ac.setStatusTip(tt)

        for ch in self.bar.children():
            if isinstance(ch, QToolButton):
                ch.setCursor(Qt.PointingHandCursor)
                ch.setAutoRaise(True)

        self.stack.setCurrentIndex(0)

        if initial_plugin is not None:
            category, name = initial_plugin
            plugin = get_plugin(category, name)
            if plugin is not None:
                self.show_plugin(plugin)

    def run_wizard(self):
        self.close()
        self.run_wizard_requested.emit()

    def set_tooltips_for_labels(self):
        def process_child(child):
            for g in child.children():
                if isinstance(g, QLabel):
                    buddy = g.buddy()
                    if buddy is not None and hasattr(buddy, 'toolTip'):
                        htext = unicode(buddy.toolTip()).strip()
                        etext = unicode(g.toolTip()).strip()
                        if htext and not etext:
                            g.setToolTip(htext)
                            g.setWhatsThis(htext)
                else:
                    process_child(g)

        process_child(self.showing_widget)

    def show_plugin(self, plugin):
        self.showing_widget = plugin.create_widget(self.scroll_area)
        self.showing_widget.genesis(self.gui)
        self.showing_widget.initialize()
        self.set_tooltips_for_labels()
        self.scroll_area.setWidget(self.showing_widget)
        self.stack.setCurrentIndex(1)
        self.showing_widget.show()
        self.setWindowTitle(__appname__ + ' - ' + _('Preferences') + ' - ' +
                            plugin.gui_name)
        self.apply_action.setEnabled(False)
        self.showing_widget.changed_signal.connect(
            lambda: self.apply_action.setEnabled(True))
        self.showing_widget.restart_now.connect(self.restart_now)
        self.restore_action.setEnabled(
            self.showing_widget.supports_restoring_to_defaults)
        tt = self.showing_widget.restore_defaults_desc
        if not self.restore_action.isEnabled():
            tt = _('Restoring to defaults not supported for') + ' ' + \
                plugin.gui_name
        self.restore_action.setToolTip(textwrap.fill(tt))
        self.restore_action.setWhatsThis(textwrap.fill(tt))
        self.restore_action.setStatusTip(tt)
        self.bar_title.show_plugin(plugin)
        self.setWindowIcon(QIcon(plugin.icon))
        self.bar.setVisible(True)
        self.bb.setVisible(False)

    def hide_plugin(self):
        self.showing_widget = QWidget(self.scroll_area)
        self.scroll_area.setWidget(self.showing_widget)
        self.setWindowTitle(__appname__ + ' - ' + _('Preferences'))
        self.bar.setVisible(False)
        self.stack.setCurrentIndex(0)
        self.setWindowIcon(QIcon(I('config.png')))
        self.bb.setVisible(True)

    def esc(self, *args):
        if self.stack.currentIndex() == 1:
            self.cancel()
        elif self.stack.currentIndex() == 0:
            self.close()

    def restart_now(self):
        try:
            self.showing_widget.commit()
        except AbortCommit:
            return
        self.hide_plugin()
        self.close()
        self.gui.quit(restart=True)

    def commit(self, *args):
        try:
            must_restart = self.showing_widget.commit()
        except AbortCommit:
            return
        rc = self.showing_widget.restart_critical
        self.committed = True
        do_restart = False
        if must_restart:
            self.must_restart = True
            msg = _('Some of the changes you made require a restart.'
                    ' Please restart calibre as soon as possible.')
            if rc:
                msg = _('The changes you have made require calibre be '
                        'restarted immediately. You will not be allowed to '
                        'set any more preferences, until you restart.')

            do_restart = show_restart_warning(msg, parent=self)

        self.showing_widget.refresh_gui(self.gui)
        self.hide_plugin()
        if self.close_after_initial or (must_restart and rc) or do_restart:
            self.close()
        if do_restart:
            self.gui.quit(restart=True)

    def cancel(self, *args):
        if self.close_after_initial:
            self.close()
        else:
            self.hide_plugin()

    def restore_defaults(self, *args):
        self.showing_widget.restore_defaults()

    def closeEvent(self, *args):
        gprefs.set('preferences_window_geometry',
                   bytearray(self.saveGeometry()))
        if self.committed:
            self.gui.must_restart_before_config = self.must_restart
            self.gui.tags_view.recount()
            self.gui.create_device_menu()
            self.gui.set_device_menu_items_state(
                bool(self.gui.device_connected))
            self.gui.bars_manager.apply_settings()
            self.gui.bars_manager.update_bars()
            self.gui.build_context_menus()

        return QMainWindow.closeEvent(self, *args)
示例#6
0
class BookInfo(QWebView):

    link_clicked = pyqtSignal(object)
    remove_format = pyqtSignal(int, object)
    remove_item = pyqtSignal(int, object, object)
    save_format = pyqtSignal(int, object)
    restore_format = pyqtSignal(int, object)
    compare_format = pyqtSignal(int, object)
    set_cover_format = pyqtSignal(int, object)
    copy_link = pyqtSignal(object)
    manage_author = pyqtSignal(object)
    open_fmt_with = pyqtSignal(int, object, object)

    def __init__(self, vertical, parent=None):
        QWebView.__init__(self, parent)
        s = self.settings()
        s.setAttribute(s.JavascriptEnabled, False)
        self.vertical = vertical
        self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
        self.linkClicked.connect(self.link_activated)
        self._link_clicked = False
        self.setAttribute(Qt.WA_OpaquePaintEvent, False)
        palette = self.palette()
        self.setAcceptDrops(False)
        palette.setBrush(QPalette.Base, Qt.transparent)
        self.page().setPalette(palette)
        for x, icon in [
            ('remove_format', 'trash.png'),
            ('save_format', 'save.png'),
            ('restore_format', 'edit-undo.png'),
            ('copy_link', 'edit-copy.png'),
            ('manage_author', 'user_profile.png'),
            ('compare_format', 'diff.png'),
            ('set_cover_format', 'default_cover.png'),
        ]:
            ac = QAction(QIcon(I(icon)), '', self)
            ac.current_fmt = None
            ac.current_url = None
            ac.triggered.connect(getattr(self, '%s_triggerred' % x))
            setattr(self, '%s_action' % x, ac)
        self.remove_item_action = ac = QAction(QIcon(I('minus.png')), '...',
                                               self)
        ac.data = (None, None, None)
        ac.triggered.connect(self.remove_item_triggered)
        self.setFocusPolicy(Qt.NoFocus)

    def remove_item_triggered(self):
        field, value, book_id = self.remove_item_action.data
        if field:
            self.remove_item.emit(book_id, field, value)

    def context_action_triggered(self, which):
        f = getattr(self, '%s_action' % which).current_fmt
        url = getattr(self, '%s_action' % which).current_url
        if f and 'format' in which:
            book_id, fmt = f
            getattr(self, which).emit(book_id, fmt)
        if url and 'link' in which:
            getattr(self, which).emit(url)

    def remove_format_triggerred(self):
        self.context_action_triggered('remove_format')

    def save_format_triggerred(self):
        self.context_action_triggered('save_format')

    def restore_format_triggerred(self):
        self.context_action_triggered('restore_format')

    def compare_format_triggerred(self):
        self.context_action_triggered('compare_format')

    def set_cover_format_triggerred(self):
        self.context_action_triggered('set_cover_format')

    def copy_link_triggerred(self):
        self.context_action_triggered('copy_link')

    def manage_author_triggerred(self):
        self.manage_author.emit(self.manage_author_action.current_fmt)

    def link_activated(self, link):
        self._link_clicked = True
        if str(link.scheme()) in ('http', 'https'):
            return open_url(link)
        link = str(link.toString(NO_URL_FORMATTING))
        self.link_clicked.emit(link)

    def turnoff_scrollbar(self, *args):
        self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal,
                                                   Qt.ScrollBarAlwaysOff)

    def show_data(self, mi):
        html = render_html(mi, css(), self.vertical, self.parent())
        self.setHtml(html)

    def mouseDoubleClickEvent(self, ev):
        swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width()
        sheight = self.page().mainFrame().scrollBarGeometry(
            Qt.Horizontal).height()
        if self.width() - ev.x() < swidth or \
            self.height() - ev.y() < sheight:
            # Filter out double clicks on the scroll bar
            ev.accept()
        else:
            ev.ignore()

    def contextMenuEvent(self, ev):
        details_context_menu_event(self, ev, self)

    def open_with(self, book_id, fmt, entry):
        self.open_fmt_with.emit(book_id, fmt, entry)

    def choose_open_with(self, book_id, fmt):
        from calibre.gui2.open_with import choose_program
        entry = choose_program(fmt, self)
        if entry is not None:
            self.open_with(book_id, fmt, entry)
示例#7
0
class Results(QWidget):

    EMPH = "color:magenta; font-weight:bold"
    MARGIN = 4

    item_selected = pyqtSignal()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent=parent)

        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.results = ()
        self.current_result = -1
        self.max_result = -1
        self.mouse_hover_result = -1
        self.setMouseTracking(True)
        self.setFocusPolicy(Qt.NoFocus)
        self.text_option = to = QTextOption()
        to.setWrapMode(to.NoWrap)
        self.divider = QStaticText('\xa0→ \xa0')
        self.divider.setTextFormat(Qt.PlainText)

    def item_from_y(self, y):
        if not self.results:
            return
        delta = self.results[0][0].size().height() + self.MARGIN
        maxy = self.height()
        pos = 0
        for i, r in enumerate(self.results):
            bottom = pos + delta
            if pos <= y < bottom:
                return i
                break
            pos = bottom
            if pos > min(y, maxy):
                break
        return -1

    def mouseMoveEvent(self, ev):
        y = ev.pos().y()
        prev = self.mouse_hover_result
        self.mouse_hover_result = self.item_from_y(y)
        if prev != self.mouse_hover_result:
            self.update()

    def mousePressEvent(self, ev):
        if ev.button() == 1:
            i = self.item_from_y(ev.pos().y())
            if i != -1:
                ev.accept()
                self.current_result = i
                self.update()
                self.item_selected.emit()
                return
        return QWidget.mousePressEvent(self, ev)

    def change_current(self, delta=1):
        if not self.results:
            return
        nc = self.current_result + delta
        if 0 <= nc <= self.max_result:
            self.current_result = nc
            self.update()

    def __call__(self, results):
        if results:
            self.current_result = 0
            prefixes = [
                QStaticText('<b>%s</b>' % os.path.basename(x)) for x in results
            ]
            [(p.setTextFormat(Qt.RichText), p.setTextOption(self.text_option))
             for p in prefixes]
            self.maxwidth = max([x.size().width() for x in prefixes])
            self.results = tuple(
                (prefix, self.make_text(text, positions), text)
                for prefix, (text,
                             positions) in izip(prefixes, results.iteritems()))
        else:
            self.results = ()
            self.current_result = -1
        self.max_result = min(10, len(self.results) - 1)
        self.mouse_hover_result = -1
        self.update()

    def make_text(self, text, positions):
        text = QStaticText(make_highlighted_text(self.EMPH, text, positions))
        text.setTextOption(self.text_option)
        text.setTextFormat(Qt.RichText)
        return text

    def paintEvent(self, ev):
        offset = QPoint(0, 0)
        p = QPainter(self)
        p.setClipRect(ev.rect())
        bottom = self.rect().bottom()

        if self.results:
            for i, (prefix, full, text) in enumerate(self.results):
                size = prefix.size()
                if offset.y() + size.height() > bottom:
                    break
                self.max_result = i
                offset.setX(0)
                if i in (self.current_result, self.mouse_hover_result):
                    p.save()
                    if i != self.current_result:
                        p.setPen(Qt.DotLine)
                    p.drawLine(offset, QPoint(self.width(), offset.y()))
                    p.restore()
                offset.setY(offset.y() + self.MARGIN // 2)
                p.drawStaticText(offset, prefix)
                offset.setX(self.maxwidth + 5)
                p.drawStaticText(offset, self.divider)
                offset.setX(offset.x() + self.divider.size().width())
                p.drawStaticText(offset, full)
                offset.setY(offset.y() + size.height() + self.MARGIN // 2)
                if i in (self.current_result, self.mouse_hover_result):
                    offset.setX(0)
                    p.save()
                    if i != self.current_result:
                        p.setPen(Qt.DotLine)
                    p.drawLine(offset, QPoint(self.width(), offset.y()))
                    p.restore()
        else:
            p.drawText(self.rect(), Qt.AlignCenter, _('No results found'))

        p.end()

    @property
    def selected_result(self):
        try:
            return self.results[self.current_result][-1]
        except IndexError:
            pass
示例#8
0
class Central(QStackedWidget):  # {{{

    ' The central widget, hosts the editors '

    current_editor_changed = pyqtSignal()
    close_requested = pyqtSignal(object)

    def __init__(self, parent=None):
        QStackedWidget.__init__(self, parent)
        self.welcome = w = QLabel(
            '<p>' + _('Double click a file in the left panel to start editing'
                      ' it.'))
        self.addWidget(w)
        w.setWordWrap(True)
        w.setAlignment(Qt.AlignTop | Qt.AlignHCenter)

        self.container = c = QWidget(self)
        self.addWidget(c)
        l = c.l = QVBoxLayout(c)
        c.setLayout(l)
        l.setContentsMargins(0, 0, 0, 0)
        self.editor_tabs = t = QTabWidget(c)
        l.addWidget(t)
        t.setDocumentMode(True)
        t.setTabsClosable(True)
        t.setMovable(True)
        pal = self.palette()
        if pal.color(pal.WindowText).lightness() > 128:
            i = QImage(I('modified.png'))
            i.invertPixels()
            self.modified_icon = QIcon(QPixmap.fromImage(i))
        else:
            self.modified_icon = QIcon(I('modified.png'))
        self.editor_tabs.currentChanged.connect(self.current_editor_changed)
        self.editor_tabs.tabCloseRequested.connect(self._close_requested)
        self.search_panel = SearchPanel(self)
        l.addWidget(self.search_panel)
        self.restore_state()
        self.editor_tabs.tabBar().installEventFilter(self)

    def _close_requested(self, index):
        editor = self.editor_tabs.widget(index)
        self.close_requested.emit(editor)

    def add_editor(self, name, editor):
        fname = name.rpartition('/')[2]
        index = self.editor_tabs.addTab(editor, fname)
        self.editor_tabs.setTabToolTip(index, _('Full path:') + ' ' + name)
        editor.modification_state_changed.connect(self.editor_modified)

    @property
    def tab_order(self):
        ans = []
        rmap = {v: k for k, v in iteritems(editors)}
        for i in range(self.editor_tabs.count()):
            name = rmap.get(self.editor_tabs.widget(i))
            if name is not None:
                ans.append(name)
        return ans

    def rename_editor(self, editor, name):
        for i in range(self.editor_tabs.count()):
            if self.editor_tabs.widget(i) is editor:
                fname = name.rpartition('/')[2]
                self.editor_tabs.setTabText(i, fname)
                self.editor_tabs.setTabToolTip(i, _('Full path:') + ' ' + name)

    def show_editor(self, editor):
        self.setCurrentIndex(1)
        self.editor_tabs.setCurrentWidget(editor)

    def close_editor(self, editor):
        for i in range(self.editor_tabs.count()):
            if self.editor_tabs.widget(i) is editor:
                self.editor_tabs.removeTab(i)
                if self.editor_tabs.count() == 0:
                    self.setCurrentIndex(0)
                return True
        return False

    def editor_modified(self, *args):
        tb = self.editor_tabs.tabBar()
        for i in range(self.editor_tabs.count()):
            editor = self.editor_tabs.widget(i)
            modified = getattr(editor, 'is_modified', False)
            tb.setTabIcon(i, self.modified_icon if modified else QIcon())

    def close_current_editor(self):
        ed = self.current_editor
        if ed is not None:
            self.close_requested.emit(ed)

    def close_all_but_current_editor(self):
        self.close_all_but(self.current_editor)

    def close_all_but(self, ed):
        close = []
        if ed is not None:
            for i in range(self.editor_tabs.count()):
                q = self.editor_tabs.widget(i)
                if q is not None and q is not ed:
                    close.append(q)
        for q in close:
            self.close_requested.emit(q)

    @property
    def current_editor(self):
        return self.editor_tabs.currentWidget()

    def save_state(self):
        tprefs.set('search-panel-visible', self.search_panel.isVisible())
        self.search_panel.save_state()
        for ed in itervalues(editors):
            ed.save_state()
        if self.current_editor is not None:
            self.current_editor.save_state(
            )  # Ensure the current editor saves it state last

    def restore_state(self):
        self.search_panel.setVisible(tprefs.get('search-panel-visible', False))
        self.search_panel.restore_state()

    def show_find(self):
        self.search_panel.show_panel()

    def pre_fill_search(self, text):
        self.search_panel.pre_fill(text)

    def eventFilter(self, obj, event):
        base = super(Central, self)
        if obj is not self.editor_tabs.tabBar() or event.type(
        ) != QEvent.MouseButtonPress or event.button() not in (Qt.RightButton,
                                                               Qt.MidButton):
            return base.eventFilter(obj, event)
        index = self.editor_tabs.tabBar().tabAt(event.pos())
        if index < 0:
            return base.eventFilter(obj, event)
        if event.button() == Qt.MidButton:
            self._close_requested(index)
        ed = self.editor_tabs.widget(index)
        if ed is not None:
            menu = QMenu(self)
            menu.addAction(actions['close-current-tab'].icon(), _('Close tab'),
                           partial(self.close_requested.emit, ed))
            menu.addSeparator()
            menu.addAction(actions['close-all-but-current-tab'].icon(),
                           _('Close other tabs'),
                           partial(self.close_all_but, ed))
            menu.exec_(self.editor_tabs.tabBar().mapToGlobal(event.pos()))

        return True
示例#9
0
class Client(QObject):
    '''Handles socket.'''

    # Emitted Signals
    comm_update = pyqtSignal([str], name='communication_update')
    chat_update = pyqtSignal([dict], name='chat_update')
    client_connected = pyqtSignal(name='client_connected')
    handshake_succeded = pyqtSignal(name='handshake_succeeded')
    handshake_failed = pyqtSignal(name='handshake_failed')
    msg_ready = pyqtSignal([dict], name='msg_ready')
    comm_error = pyqtSignal(name='comm_error')

    # Signals listened to
    # data_ready = pyqtSignal([str, dict], name='data_ready')

    def __init__(self, parent):
        QObject.__init__(self, parent)
        self._parent = parent
        self._km = parent.key_manager
        self._codec = parent.codec
        self._VERSION = parent.VERSION
        self._qsocket = QTcpSocket(self)
        self._bytestream = None
        self._other_party = None
        self._this_party = None
        self._ip_address = None
        self._port = None
        self._setup()

    def _setup(self):
        # Ready read
        self._qsocket.readyRead.connect(self._on_ready_read)
        # On connection
        self._qsocket.connected.connect(self._on_connection)
        # On disconnection
        self._qsocket.disconnected.connect(self._on_disconnection)
        # Prime the json generator
        self._msg_buffer = self._add_to_buffer()
        self._msg_buffer.send(None)
        # Connect self.msg_ready
        self.msg_ready.connect(self._dispatch_msg)
        # COnnect connection to _client_handshake_1 to start it off.
        self._qsocket.connected.connect(self._client_handshake_1)
        # Connect error to emit socket update.
        self._qsocket.error.connect(self._on_error)

    def _on_connection(self):
        name = str(self._qsocket.peerName())
        address = self._qsocket.peerAddress().toString()
        self.comm_update.emit('Connected to ' + name + ' (' + address + ')' +
                              '.')

    def _on_disconnection(self):
        self.comm_update.emit('Connection terminated.')
        self.comm_error.emit()

    def _on_error(self):
        self.comm_update.emit(self._qsocket.errorString() + '.')
        self.comm_error.emit()

    def _on_ready_read(self):
        data = self._qsocket.read(4096)
        self._msg_buffer.send(data)
        # Re-emit readyRead, quasi-recursive.
        if self._qsocket.bytesAvailable():
            self._qsocket.readyRead.emit()

    def _add_to_buffer(self):
        '''Generator created in _setup. Issues data via signal.'''
        # Set up constants.
        opener = '{'  #123
        closer = '}'  #125
        single_quote = '\''  # 39
        double_quote = '\"'  # 34
        self._bytestream = io.BytesIO()
        inside_double_quote = False
        inside_single_quote = False
        opener_count = 0
        closer_count = 0
        # Repeat this loop until end of time.
        while True:
            # Given via generator.send method.
            raw_data = yield
            data = raw_data.decode('utf-8')
            for character in data:
                if character == single_quote:
                    # If True, False. If False, True.
                    inside_single_quote = not inside_single_quote
                if character == double_quote:
                    inside_double_quote = not inside_double_quote
                if inside_single_quote or inside_double_quote:
                    pass
                else:
                    if character == opener:
                        opener_count += 1
                    if character == closer:
                        closer_count += 1
                    if opener_count == closer_count:
                        # Write final character to bytestream
                        self._bytestream.write(character.encode())
                        # Complete JSON message.
                        json_msg = self._bytestream.getvalue().decode('utf-8')
                        msg = json.loads(json_msg)
                        self.msg_ready.emit(msg)
                        # Reset because it's a new message now.
                        self._bytestream = io.BytesIO()
                        inside_double_quote = False
                        inside_single_quote = False
                        opener_count = 0
                        closer_count = 0
                        # Back to "while True" loop
                        continue
                # Write to bytestream
                self._bytestream.write(character.encode())

    def connect_(self, identity, ip_address, port, numeric=True):
        self.comm_update.emit('Connecting.')
        self._this_party = identity
        # If connected, disconnect.
        try:
            if self._qsocket.state == 3:
                self._qsocket.disconnectFromHost()
        except AttributeError:
            pass
        # Connect to host
        if numeric:
            self._qsocket.connectToHost(QHostAddress(ip_address), int(port))
        else:
            self._qsocket.connectToHost(ip_address, int(port))

    def _dispatch_msg(self, msg_dict):
        '''Route message to correct function.'''
        msg_type = msg_dict['metadata']['type']
        routing_dict = {
            'text': self._recv_text,
            'file': None,
            'server_handshake_1': self._client_handshake_2,
            'server_handshake_2': self._client_handshake_3
        }
        function_to_use = routing_dict[msg_type]
        function_to_use(msg_dict)

    def _client_handshake_1(self):
        # Client sends client handshake 1 with list of keys (first msg).
        key_list = [key.name for key in self._km.get_all_keys()]
        data_dict = {'key_list': key_list}
        client_msg_1 = self._create_json_msg('client_handshake_1', data_dict)
        self._qsocket.write(client_msg_1.encode())

    def _client_handshake_2(self, msg_dict):
        self._other_party = msg_dict['metadata']['sender']
        shared_keys = msg_dict['data']['shared_keys']
        client_shared_key_data = [
            self._km.get_key_data(key) for key in shared_keys
        ]
        data_dict = {'client_shared_key_data': client_shared_key_data}
        client_msg_2 = self._create_json_msg('client_handshake_2', data_dict)
        self._qsocket.write(client_msg_2.encode())

    def _client_handshake_3(self, msg_dict):
        '''No message.'''
        # Server sends server handshake 2.
        harmonized_data = msg_dict['data']['harmonized_data']
        if not harmonized_data:
            self.comm_update.emit('No shared keys.')
            self._qsocket.disconnectFromHost()
            return False
        for item in harmonized_data:
            if item['sender'] == self._this_party:
                self._km.add_to_outbound(item['name'])
            if item['sender'] == self._other_party:
                self._km.add_to_inbound(item['name'])
        self.handshake_succeded.emit()
        self.comm_update.emit('Client-side handshake complete.')

    def _recv_text(self, msg):
        ciphertext = msg['data']['ciphertext']
        cipherhash = msg['data']['cipherhash']
        plaintext = self._codec.decode(ciphertext, cipherhash)
        msg['plaintext'] = plaintext
        msg['source'] = self._other_party
        self.chat_update.emit(msg)

    def send_text(self, text_data):
        data_bytes = text_data.encode('utf-8')
        ciphertext, cipherhash = self._codec.encode(data_bytes)
        data_dict = {
            'ciphertext': base64.b64encode(ciphertext).decode(),
            'cipherhash': base64.b64encode(cipherhash).decode()
        }
        msg = self._create_json_msg('text', data_dict)
        self._qsocket.write(msg.encode())
        self.chat_update.emit({
            'source': self._this_party,
            'content': text_data
        })

    def _send_file(self, file_object):
        pass

    def _recv_file(self, output_path):
        pass

    def _create_json_msg(self, msg_type, data_dict):
        '''Create msg based on template (must be json serialize-able).'''
        base = {
            'metadata': {
                'brickton_version': self._VERSION,
                'sender': self._this_party,
                'receiver': self._other_party,
                'timestamp': '{0:.7f}'.format(time.time()),
                'encoding': 'base64',
                'type': msg_type
            },
            'data': {}
        }
        # Types to check against.
        types = [
            'text', 'file', 'server_handshake_1', 'server_handshake_2',
            'client_handshake_1', 'client_handshake_2'
        ]
        # Type error checking.
        if not msg_type in types:
            raise BricktonError('Message type not valid.')
        # Keys by type.
        keys_by_type = {
            'text': ['ciphertext', 'cipherhash'],
            'file': ['ciphertext', 'cipherhash', 'ciphername'],
            'server_handshake_1': ['shared_keys'],
            'server_handshake_2': ['harmonized_data'],
            'client_handshake_1': ['key_list'],
            'client_handshake_2': ['client_shared_key_data']
        }
        # Keys by type error checking.
        for key in data_dict.keys():
            if not key in keys_by_type[msg_type]:
                raise BricktonError('Improper data for message type supplied.')
        # Populate data dict.
        for key, value in data_dict.items():
            base['data'][key] = value
        # Create and return msg as json.
        msg_string = json.dumps(base)
        return msg_string

    def stop(self):
        self._qsocket.close()
        self.terminate()
示例#10
0
class Main(
        MainWindow,
        MainWindowMixin,
        DeviceMixin,
        EmailMixin,  # {{{
        TagBrowserMixin,
        CoverFlowMixin,
        LibraryViewMixin,
        SearchBoxMixin,
        SavedSearchBoxMixin,
        SearchRestrictionMixin,
        LayoutMixin,
        UpdateMixin,
        EbookDownloadMixin):

    'The main GUI'

    proceed_requested = pyqtSignal(object, object)
    book_converted = pyqtSignal(object, object)
    shutting_down = False

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self,
                            opts,
                            parent=parent,
                            disable_automatic_gc=True)
        self.setWindowIcon(QApplication.instance().windowIcon())
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed,
                                       type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores
        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.library_broker = GuiLibraryBroker(db)
        self.content_server = None
        self.server_change_notification_timer = t = QTimer(self)
        self.server_changes = Queue()
        t.setInterval(1000), t.timeout.connect(
            self.handle_changes_from_server_debounced), t.setSingleShot(True)
        self._spare_pool = None
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.check_messages_timer.timeout.connect(
            self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback
                traceback.print_exc()
                if getattr(ac, 'plugin_path', None) is None:
                    raise
        self.donate_action = QAction(QIcon(I('donate.png')),
                                     _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.init_main_window_mixin(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.init_layout_mixin(self)
        DeviceMixin.init_device_mixin(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = None
        if config['systray_icon']:
            self.system_tray_icon = factory(
                app_id='com.calibre-ebook.gui').create_system_tray_icon(
                    parent=self, title='calibre')
        if self.system_tray_icon is not None:
            self.system_tray_icon.setIcon(
                QIcon(I('lt.png', allow_user_override=False)))
            if not (iswindows or isosx):
                self.system_tray_icon.setIcon(
                    QIcon.fromTheme('calibre-gui',
                                    self.system_tray_icon.icon()))
            self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip())
            self.system_tray_icon.setVisible(True)
            self.jobs_button.tray_tooltip_updated.connect(
                self.system_tray_icon.setToolTip)
        elif config['systray_icon']:
            prints(
                'Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec'
            )
        self.system_tray_menu = QMenu(self)
        self.toggle_to_tray_action = self.system_tray_menu.addAction(
            QIcon(I('page.png')), '')
        self.toggle_to_tray_action.triggered.connect(
            self.system_tray_icon_activated)
        self.system_tray_menu.addAction(self.donate_action)
        self.eject_action = self.system_tray_menu.addAction(
            QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre',
                                        _('Quit calibre'),
                                        default_keys=('Ctrl+Q', ),
                                        action=self.quit_action)
        if self.system_tray_icon is not None:
            self.system_tray_icon.setContextMenu(self.system_tray_menu)
            self.system_tray_icon.activated.connect(
                self.system_tray_icon_activated)
        self.quit_action.triggered[bool].connect(self.quit)
        self.donate_action.triggered[bool].connect(self.donate)
        self.minimize_action = QAction(_('Minimize the calibre window'), self)
        self.addAction(self.minimize_action)
        self.keyboard.register_shortcut('minimize calibre',
                                        self.minimize_action.text(),
                                        default_keys=(),
                                        action=self.minimize_action)
        self.minimize_action.triggered.connect(self.showMinimized)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                                        _('Clear the current search'),
                                        default_keys=('Esc', ),
                                        action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut('focus book list',
                                        _('Focus the book list'),
                                        default_keys=('Shift+Esc', ),
                                        action=self.shift_esc_action)
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut('clear virtual library',
                                        _('Clear the virtual library'),
                                        default_keys=('Ctrl+Esc', ),
                                        action=self.ctrl_esc_action)
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut('clear additional restriction',
                                        _('Clear the additional restriction'),
                                        default_keys=('Alt+Esc', ),
                                        action=self.alt_esc_action)
        self.alt_esc_action.triggered.connect(
            self.clear_additional_restriction)

        # ###################### Start spare job server ########################
        QTimer.singleShot(1000, self.create_spare_pool)

        # ###################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(
            self.device_manager.umount_device)
        self.location_manager.configure_device.connect(
            self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(
            self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        # ################### Update notification ###################
        UpdateMixin.init_update_mixin(self, opts)

        # ###################### Search boxes ########################
        SearchRestrictionMixin.init_search_restriction_mixin(self)
        SavedSearchBoxMixin.init_saved_seach_box_mixin(self)

        # ###################### Library view ########################
        LibraryViewMixin.init_library_view_mixin(self, db)
        SearchBoxMixin.init_search_box_mixin(self)  # Requires current_db

        self.library_view.model().count_changed_signal.connect(
            self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            try:
                add_quick_start_guide(self.library_view)
            except:
                import traceback
                traceback.print_exc()
        for view in ('library', 'memory', 'card_a', 'card_b'):
            v = getattr(self, '%s_view' % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(
            self.bars_manager.database_changed, type=Qt.QueuedConnection)

        # ########################## Tags Browser ##############################
        TagBrowserMixin.init_tag_browser_mixin(self, db)
        self.library_view.model().database_changed.connect(
            self.populate_tb_manage_menu, type=Qt.QueuedConnection)

        # ######################## Search Restriction ##########################
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()

        # ########################## Cover Flow ################################

        CoverFlowMixin.init_cover_flow_mixin(self)

        self._calculated_available_height = min(max_available_height() - 15,
                                                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.read_settings()

        self.finalize_layout()
        self.bars_manager.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.set_current_library_information(current_library_name(),
                                             db.library_id, db.field_metadata)

        register_keyboard_shortcuts()
        self.keyboard.finalize()
        if show_gui:
            # Note this has to come after restoreGeometry() because of
            # https://bugreports.qt.io/browse/QTBUG-56831
            self.show()
        if self.system_tray_icon is not None and self.system_tray_icon.isVisible(
        ) and opts.start_in_tray:
            self.hide_windows()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)
        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        QApplication.instance().shutdown_signal_received.connect(self.quit)
        if show_gui and self.gui_debug is not None:
            QTimer.singleShot(10, self.show_gui_debug_msg)

        self.iactions['Connect Share'].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)
        QTimer.singleShot(100, self.update_toggle_to_tray_action)

    def show_gui_debug_msg(self):
        info_dialog(self,
                    _('Debug mode'),
                    '<p>' +
                    _('You have started calibre in debug mode. After you '
                      'quit calibre, the debug log will be available in '
                      'the file: %s<p>The '
                      'log will be displayed automatically.') % self.gui_debug,
                    show=True)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option('smartdevice', 'autostart'):
            try:
                message = self.device_manager.start_plugin('smartdevice')
            except:
                message = 'start smartdevice unknown exception'
                prints(message)
                import traceback
                traceback.print_exc()
        if message:
            if not self.device_manager.is_running('Wireless Devices'):
                error_dialog(
                    self,
                    _('Problem starting the wireless device'),
                    _('The wireless device driver had problems starting. '
                      'It said "%s"') % message,
                    show=True)
        self.iactions['Connect Share'].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.srv.embedded import Server
        self.content_server = Server(
            self.library_broker, Dispatcher(self.handle_changes_from_server))
        self.content_server.state_callback = Dispatcher(
            self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)
        self.content_server.start()

    def handle_changes_from_server(self, library_path, change_event):
        if DEBUG:
            prints('Received server change event: {} for {}'.format(
                change_event, library_path))
        if self.library_broker.is_gui_library(library_path):
            self.server_changes.put((library_path, change_event))
            self.server_change_notification_timer.start()

    def handle_changes_from_server_debounced(self):
        if self.shutting_down:
            return
        changes = []
        while True:
            try:
                library_path, change_event = self.server_changes.get_nowait()
            except Empty:
                break
            if self.library_broker.is_gui_library(library_path):
                changes.append(change_event)
        if changes:
            handle_changes(changes, self)

    def content_server_start_failed(self, msg):
        error_dialog(self,
                     _('Failed to start Content server'),
                     _('Could not start the Content server. Error:\n\n%s') %
                     msg,
                     show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width() - 150)

    def create_spare_pool(self, *args):
        if self._spare_pool is None:
            num = min(detect_ncpus(), int(config['worker_limit'] / 2.0))
            self._spare_pool = Pool(max_workers=num, name='GUIPool')

    def spare_pool(self):
        ans, self._spare_pool = self._spare_pool, None
        QTimer.singleShot(1000, self.create_spare_pool)
        return ans

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r=False):
        if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False):
            if self.isVisible():
                if self.isMinimized():
                    self.showNormal()
                else:
                    self.hide_windows()
            else:
                self.show_windows()
                if self.isMinimized():
                    self.showNormal()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self,
                              title,
                              msg,
                              det_msg='',
                              show_copy_button=False,
                              ans_when_user_unavailable=True,
                              skip_dialog_name=None,
                              skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self,
                               title,
                               msg,
                               det_msg=det_msg,
                               show_copy_button=show_copy_button,
                               skip_dialog_name=skip_dialog_name,
                               skip_dialog_skipped_value=skipped_value)

    def update_toggle_to_tray_action(self, *args):
        if hasattr(self, 'toggle_to_tray_action'):
            self.toggle_to_tray_action.setText(
                _('Hide main window') if self.isVisible(
                ) else _('Show main window'))

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)
        self.update_toggle_to_tray_action()

    def show_windows(self, *args):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)
        self.update_toggle_to_tray_action()

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start Content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def refresh_all(self):
        m = self.library_view.model()
        m.db.data.refresh(clear_caches=False, do_search=False)
        self.saved_searches_changed(recount=False)
        m.resort()
        m.research()
        self.tags_view.recount()

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            import json
            try:
                argv = json.loads(msg[len('launched:'):])
            except ValueError:
                prints('Failed to decode message from other instance: %r' %
                       msg)
                if DEBUG:
                    error_dialog(
                        self,
                        'Invalid message',
                        'Received an invalid message from other calibre instance.'
                        ' Do you have multiple versions of calibre installed?',
                        det_msg='Invalid msg: %r' % msg,
                        show=True)
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [
                    os.path.abspath(p) for p in argv[1:]
                    if not os.path.isdir(p) and os.access(p, os.R_OK)
                ]
                if files:
                    self.iactions['Add Books'].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized
                                | Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            self.refresh_all()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        elif msg.startswith('bookedited:'):
            parts = msg.split(':')[1:]
            try:
                book_id, fmt, library_id = parts[:3]
                book_id = int(book_id)
                m = self.library_view.model()
                db = m.db.new_api
                if m.db.library_id == library_id and db.has_id(book_id):
                    db.format_metadata(book_id,
                                       fmt,
                                       allow_cache=False,
                                       update_db=True)
                    db.update_last_modified((book_id, ))
                    m.refresh_ids((book_id, ))
            except Exception:
                import traceback
                traceback.print_exc()
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model(
        ).db, self.card_b_view.model().db

    def library_moved(self, newloc, copy_structure=False, allow_rebuild=False):
        if newloc is None:
            return
        with self.library_broker:
            default_prefs = None
            try:
                olddb = self.library_view.model().db
                if copy_structure:
                    default_prefs = olddb.prefs
            except:
                olddb = None
            if copy_structure and olddb is not None and default_prefs is not None:
                default_prefs[
                    'field_metadata'] = olddb.new_api.field_metadata.all_metadata(
                    )
            db = self.library_broker.prepare_for_gui_library_change(newloc)
            if db is None:
                try:
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                except apsw.Error:
                    if not allow_rebuild:
                        raise
                    import traceback
                    repair = question_dialog(
                        self,
                        _('Corrupted database'),
                        _('The library database at %s appears to be corrupted. Do '
                          'you want calibre to try and rebuild it automatically? '
                          'The rebuild may not be completely successful.') %
                        force_unicode(newloc, filesystem_encoding),
                        det_msg=traceback.format_exc())
                    if repair:
                        from calibre.gui2.dialogs.restore_library import repair_library_at
                        if repair_library_at(newloc, parent=self):
                            db = LibraryDatabase(newloc,
                                                 default_prefs=default_prefs)
                        else:
                            return
                    else:
                        return
            self.library_path = newloc
            prefs['library_path'] = self.library_path
            self.book_on_device(None, reset=True)
            db.set_book_on_device_func(self.book_on_device)
            self.library_view.set_database(db)
            self.tags_view.set_database(db, self.alter_tb)
            self.library_view.model().set_book_on_device_func(
                self.book_on_device)
            self.status_bar.clear_message()
            self.search.clear()
            self.saved_search.clear()
            self.book_details.reset_info()
            # self.library_view.model().count_changed()
            db = self.library_view.model().db
            self.iactions['Choose Library'].count_changed(db.count())
            self.set_window_title()
            self.apply_named_search_restriction(
                '')  # reset restriction to null
            self.saved_searches_changed(
                recount=False)  # reload the search restrictions combo box
            if db.prefs['virtual_lib_on_startup']:
                self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
            self.rebuild_vl_tabs()
            for action in self.iactions.values():
                action.library_changed(db)
            self.library_broker.gui_library_changed(db, olddb)
            if self.device_connected:
                self.set_books_in_library(self.booklists(), reset=True)
                self.refresh_ondevice()
                self.memory_view.reset()
                self.card_a_view.reset()
                self.card_b_view.reset()
            self.set_current_library_information(current_library_name(),
                                                 db.library_id,
                                                 db.field_metadata)
            self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [
            x for x in (db.data.get_base_restriction_name(),
                        db.data.get_search_restriction_name()) if x
        ]
        restrictions = ' :: '.join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = ' :: ' + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u'{0} - || {1}{2} ||'.format(
            __appname__, self.iactions['Choose Library'].library_name(),
            restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x + '_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
            self.vl_tabs.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            self.vl_tabs.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self,
                      job,
                      dialog_title=_('Conversion error'),
                      retry_func=None):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(
                        self,
                        _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s') % title
                msg += '<p>' + _('''
                Many older e-book reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under <i>EPUB output</i> in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your e-book reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self,
                                     _('Conversion Failed'),
                                     msg,
                                     det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.mobi.reader.mobi6.KFXError:' in job.details:
                if not minz:
                    title = job.description.split(':')[-1].partition(
                        '(')[-1][:-1]
                    msg = _('<p><b>Failed to convert: %s') % title
                    idx = job.details.index(
                        'calibre.ebooks.mobi.reader.mobi6.KFXError:')
                    msg += '<p>' + re.sub(
                        r'(https:\S+)', r'<a href="\1">{}</a>'.format(
                            _('here')),
                        job.details[idx:].partition(':')[2].strip())
                    d = error_dialog(self,
                                     _('Conversion failed'),
                                     msg,
                                     det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.
                              find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                                     '<p>%s</p>' % msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:'
                    )[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {
                        'info': info_dialog,
                        'warn': warning_dialog,
                        'error': error_dialog
                    }.get(payload['level'], error_dialog)
                    d = d(self,
                          payload['title'],
                          '<p>%s</p>' % payload['msg'],
                          det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(dialog_title,
                                             _('<b>Failed</b>') + ': ' +
                                             unicode(job.description),
                                             det_msg=job.details,
                                             retry_func=retry_func)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set('main_window_geometry', self.saveGeometry())
            dynamic.set('sort_history', self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self,
             checked=True,
             restart=False,
             debug_on_restart=False,
             confirm_quit=True):
        if self.shutting_down:
            return
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('https://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False

        if self.proceed_question.questions:
            msg = _(
                'There are library updates waiting. Are you sure you want to quit?'
            )
            if not question_dialog(self, _('Library updates waiting'), msg):
                return False

        from calibre.db.delete_service import has_jobs
        if has_jobs():
            msg = _('Some deleted books are still being moved to the Recycle '
                    'Bin, if you quit now, they will be left behind. Are you '
                    'sure you want to quit?')
            if not question_dialog(self, _('Active jobs'), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.shutting_down = True
        self.show_shutdown_message()
        self.server_change_notification_timer.stop()

        from calibre.customize.ui import has_library_closed_plugins
        if has_library_closed_plugins():
            self.show_shutdown_message(
                _('Running database shutdown plugins. This could take a few seconds...'
                  ))

        self.grid_view.shutdown()
        db = None
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.new_api.set_pref('field_metadata',
                                db.field_metadata.all_metadata())
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        if hasattr(self, 'update_checker'):
            self.update_checker.shutdown()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        # Do not report any errors that happen after the shutdown
        # We cannot restore the original excepthook as that causes PyQt to
        # call abort() on unhandled exceptions
        import traceback

        def eh(t, v, tb):
            try:
                traceback.print_exception(t, v, tb, file=sys.stderr)
            except:
                pass

        sys.excepthook = eh

        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        self.library_view.model().close()

        try:
            try:
                if self.content_server is not None:
                    # If the Content server has any sockets being closed then
                    # this can take quite a long time (minutes). Tell the user that it is
                    # happening.
                    self.show_shutdown_message(
                        _('Shutting down the Content server. This could take a while...'
                          ))
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        self.hide_windows()
        if self._spare_pool is not None:
            self._spare_pool.shutdown()
        from calibre.db.delete_service import shutdown
        shutdown()
        time.sleep(2)
        self.istores.join()
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        if self.shutting_down:
            return
        self.write_settings()
        if self.system_tray_icon is not None and self.system_tray_icon.isVisible(
        ):
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(
                    self,
                    'calibre',
                    'calibre ' +
                    _('will keep running in the system tray. To close it, '
                      'choose <b>Quit</b> in the context menu of the '
                      'system tray.'),
                    show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback
                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
示例#11
0
class ManageFonts(Dialog):

    container_changed = pyqtSignal()
    embed_all_fonts = pyqtSignal()
    subset_all_fonts = pyqtSignal()

    def __init__(self, parent=None):
        Dialog.__init__(self, _('Manage Fonts'), 'manage-fonts', parent=parent)

    def setup_ui(self):
        self.setAttribute(Qt.WA_DeleteOnClose, False)
        self.l = l = QVBoxLayout(self)
        self.setLayout(l)

        self.bb.clear()
        self.bb.addButton(self.bb.Close)
        self.splitter = s = QSplitter(self)
        l.addWidget(s), l.addWidget(self.bb)

        self.fonts_view = fv = QTableView(self)
        self.model = m = AllFonts(fv)
        fv.horizontalHeader().setStretchLastSection(True)
        fv.setModel(m)
        fv.setSortingEnabled(True)
        fv.setShowGrid(False)
        fv.setAlternatingRowColors(True)
        fv.setSelectionMode(fv.ExtendedSelection)
        fv.setSelectionBehavior(fv.SelectRows)
        fv.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder)
        self.container = c = QWidget()
        l = c.l = QVBoxLayout(c)
        c.setLayout(l)
        s.addWidget(fv), s.addWidget(c)

        self.cb = b = QPushButton(_('&Change selected fonts'))
        b.setIcon(QIcon(I('auto_author_sort.png')))
        b.clicked.connect(self.change_fonts)
        l.addWidget(b)
        self.rb = b = QPushButton(_('&Remove selected fonts'))
        b.clicked.connect(self.remove_fonts)
        b.setIcon(QIcon(I('trash.png')))
        l.addWidget(b)
        self.eb = b = QPushButton(_('&Embed all fonts'))
        b.setIcon(QIcon(I('embed-fonts.png')))
        b.clicked.connect(self.embed_fonts)
        l.addWidget(b)
        self.sb = b = QPushButton(_('&Subset all fonts'))
        b.setIcon(QIcon(I('subset-fonts.png')))
        b.clicked.connect(self.subset_fonts)
        l.addWidget(b)
        self.refresh_button = b = self.bb.addButton(_('&Refresh'),
                                                    self.bb.ActionRole)
        b.setToolTip(
            _('Rescan the book for fonts in case you have made changes'))
        b.setIcon(QIcon(I('view-refresh.png')))
        b.clicked.connect(self.refresh)

        self.la = la = QLabel('<p>' + _(
            ''' All the fonts declared in this book are shown to the left, along with whether they are embedded or not.
            You can remove or replace any selected font and also embed any declared fonts that are not already embedded.'''
        ))
        la.setWordWrap(True)
        l.addWidget(la)

        l.setAlignment(Qt.AlignTop | Qt.AlignHCenter)

    def sizeHint(self):
        return Dialog.sizeHint(self) + QSize(100, 50)

    def display(self):
        if not self.isVisible():
            self.show()
        self.raise_()
        QTimer.singleShot(0, self.model.build)

    def get_selected_data(self):
        ans = self.model.data_for_indices(
            list(self.fonts_view.selectedIndexes()))
        if not ans:
            error_dialog(
                self,
                _('No fonts selected'),
                _('No fonts selected, you must first select some fonts in the left panel'
                  ),
                show=True)
        return ans

    def change_fonts(self):
        fonts = self.get_selected_data()
        if not fonts:
            return
        d = ChangeFontFamily(', '.join(fonts), {
            f
            for f, embedded in self.model.font_data.iteritems() if embedded
        }, self)
        if d.exec_() != d.Accepted:
            return
        changed = False
        new_family = d.normalized_family
        for font in fonts:
            changed |= change_font(current_container(), font, new_family)
        if changed:
            self.model.build()
            self.container_changed.emit()

    def remove_fonts(self):
        fonts = self.get_selected_data()
        if not fonts:
            return
        changed = False
        for font in fonts:
            changed |= change_font(current_container(), font)
        if changed:
            self.model.build()
            self.container_changed.emit()

    def embed_fonts(self):
        self.embed_all_fonts.emit()

    def subset_fonts(self):
        self.subset_all_fonts.emit()

    def refresh(self):
        self.model.build()
示例#12
0
class WebView(RestartingWebEngineView):

    cfi_changed = pyqtSignal(object)
    reload_book = pyqtSignal()
    toggle_toc = pyqtSignal()
    toggle_bookmarks = pyqtSignal()
    toggle_inspector = pyqtSignal()
    toggle_lookup = pyqtSignal()
    update_current_toc_nodes = pyqtSignal(object, object)
    toggle_full_screen = pyqtSignal()
    ask_for_open = pyqtSignal(object)
    selection_changed = pyqtSignal(object)
    view_image = pyqtSignal(object)

    def __init__(self, parent=None):
        self._host_widget = None
        self.callback_id_counter = count()
        self.callback_map = {}
        self.current_cfi = None
        RestartingWebEngineView.__init__(self, parent)
        self.dead_renderer_error_shown = False
        self.render_process_failed.connect(self.render_process_died)
        w = QApplication.instance().desktop().availableGeometry(self).width()
        self._size_hint = QSize(int(w / 3), int(w / 2))
        self._page = WebPage(self)
        self.bridge.bridge_ready.connect(self.on_bridge_ready)
        self.bridge.set_session_data.connect(self.set_session_data)
        self.bridge.reload_book.connect(self.reload_book)
        self.bridge.toggle_toc.connect(self.toggle_toc)
        self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks)
        self.bridge.toggle_inspector.connect(self.toggle_inspector)
        self.bridge.toggle_lookup.connect(self.toggle_lookup)
        self.bridge.update_current_toc_nodes.connect(
            self.update_current_toc_nodes)
        self.bridge.toggle_full_screen.connect(self.toggle_full_screen)
        self.bridge.ask_for_open.connect(self.ask_for_open)
        self.bridge.selection_changed.connect(self.selection_changed)
        self.bridge.view_image.connect(self.view_image)
        self.bridge.report_cfi.connect(self.call_callback)
        self.pending_bridge_ready_actions = {}
        self.setPage(self._page)
        self.setAcceptDrops(False)
        self.setUrl(QUrl('{}://{}/'.format(FAKE_PROTOCOL, FAKE_HOST)))
        self.urlChanged.connect(self.url_changed)
        if parent is not None:
            self.inspector = Inspector(
                parent.inspector_dock.toggleViewAction(), self)
            parent.inspector_dock.setWidget(self.inspector)

    def url_changed(self, url):
        if url.hasFragment():
            frag = url.fragment(url.FullyDecoded)
            if frag and frag.startswith('bookpos='):
                cfi = frag[len('bookpos='):]
                if cfi:
                    self.current_cfi = cfi
                    self.cfi_changed.emit(cfi)

    @property
    def host_widget(self):
        ans = self._host_widget
        if ans is not None and not sip.isdeleted(ans):
            return ans

    def change_zoom_by(self, steps=1):
        # TODO: Add UI for this
        ss = vprefs['session_data'].get('zoom_step_size') or 20
        amt = (ss / 100) * steps
        self._page.setZoomFactor(self._page.zoomFactor() + amt)

    def render_process_died(self):
        if self.dead_renderer_error_shown:
            return
        self.dead_renderer_error_shown = True
        error_dialog(self,
                     _('Render process crashed'),
                     _('The Qt WebEngine Render process has crashed.'
                       ' You should try restarting the viewer.'),
                     show=True)

    def event(self, event):
        if event.type() == event.ChildPolished:
            child = event.child()
            if 'HostView' in child.metaObject().className():
                self._host_widget = child
                self._host_widget.setFocus(Qt.OtherFocusReason)
        return QWebEngineView.event(self, event)

    def sizeHint(self):
        return self._size_hint

    def refresh(self):
        self.pageAction(QWebEnginePage.ReloadAndBypassCache).trigger()

    @property
    def bridge(self):
        return self._page.bridge

    def on_bridge_ready(self):
        f = QApplication.instance().font()
        self.bridge.create_view(vprefs['session_data'],
                                QFontDatabase().families(),
                                field_metadata.all_metadata(), f.family(),
                                f.pointSize())
        for func, args in iteritems(self.pending_bridge_ready_actions):
            getattr(self.bridge, func)(*args)

    def start_book_load(self, initial_cfi=None, initial_toc_node=None):
        key = (set_book_path.path, )
        self.execute_when_ready('start_book_load', key, initial_cfi,
                                initial_toc_node, set_book_path.pathtoebook)

    def execute_when_ready(self, action, *args):
        if self.bridge.ready:
            getattr(self.bridge, action)(*args)
        else:
            self.pending_bridge_ready_actions[action] = args

    def show_preparing_message(self):
        msg = _('Preparing book for first read, please wait') + '…'
        self.execute_when_ready('show_preparing_message', msg)

    def goto_toc_node(self, node_id):
        self.execute_when_ready('goto_toc_node', node_id)

    def goto_cfi(self, cfi):
        self.execute_when_ready('goto_cfi', cfi)

    def notify_full_screen_state_change(self, in_fullscreen_mode):
        self.execute_when_ready('full_screen_state_changed',
                                in_fullscreen_mode)

    def set_session_data(self, key, val):
        if key == '*' and val is None:
            vprefs['session_data'] = {}
            apply_font_settings(self._page)
        elif key != '*':
            sd = vprefs['session_data']
            sd[key] = val
            vprefs['session_data'] = sd
            if key in ('standalone_font_settings', 'base_font_size'):
                apply_font_settings(self._page)

    def do_callback(self, func_name, callback):
        cid = next(self.callback_id_counter)
        self.callback_map[cid] = callback
        self.execute_when_ready('get_current_cfi', cid)

    def call_callback(self, request_id, data):
        callback = self.callback_map.pop(request_id, None)
        if callback is not None:
            callback(data)

    def get_current_cfi(self, callback):
        self.do_callback('get_current_cfi', callback)

    def show_home_page(self):
        self.execute_when_ready('show_home_page')
示例#13
0
class Preview(QWidget):

    sync_requested = pyqtSignal(object, object)
    split_requested = pyqtSignal(object, object, object)
    split_start_requested = pyqtSignal()
    link_clicked = pyqtSignal(object, object)
    refresh_starting = pyqtSignal()
    refreshed = pyqtSignal()

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.l = l = QVBoxLayout()
        self.setLayout(l)
        l.setContentsMargins(0, 0, 0, 0)
        self.view = WebView(self)
        self.view.page().sync_requested.connect(self.request_sync)
        self.view.page().split_requested.connect(self.request_split)
        self.view.page().loadFinished.connect(self.load_finished)
        self.inspector = self.view.inspector
        self.inspector.setPage(self.view.page())
        l.addWidget(self.view)
        self.bar = QToolBar(self)
        l.addWidget(self.bar)

        ac = actions['auto-reload-preview']
        ac.setCheckable(True)
        ac.setChecked(True)
        ac.toggled.connect(self.auto_reload_toggled)
        self.auto_reload_toggled(ac.isChecked())
        self.bar.addAction(ac)

        ac = actions['sync-preview-to-editor']
        ac.setCheckable(True)
        ac.setChecked(True)
        ac.toggled.connect(self.sync_toggled)
        self.sync_toggled(ac.isChecked())
        self.bar.addAction(ac)

        self.bar.addSeparator()

        ac = actions['split-in-preview']
        ac.setCheckable(True)
        ac.setChecked(False)
        ac.toggled.connect(self.split_toggled)
        self.split_toggled(ac.isChecked())
        self.bar.addAction(ac)

        ac = actions['reload-preview']
        ac.triggered.connect(self.refresh)
        self.bar.addAction(ac)

        actions['preview-dock'].toggled.connect(self.visibility_changed)

        self.current_name = None
        self.last_sync_request = None
        self.refresh_timer = QTimer(self)
        self.refresh_timer.timeout.connect(self.refresh)
        parse_worker.start()
        self.current_sync_request = None

        self.search = HistoryLineEdit2(self)
        self.search.initialize('tweak_book_preview_search')
        self.search.setPlaceholderText(_('Search in preview'))
        self.search.returnPressed.connect(partial(self.find, 'next'))
        self.bar.addSeparator()
        self.bar.addWidget(self.search)
        for d in ('next', 'prev'):
            ac = actions['find-%s-preview' % d]
            ac.triggered.connect(partial(self.find, d))
            self.bar.addAction(ac)

    def find(self, direction):
        text = unicode(self.search.text())
        self.view.findText(
            text, QWebPage.FindWrapsAroundDocument |
            (QWebPage.FindBackward
             if direction == 'prev' else QWebPage.FindFlags(0)))

    def request_sync(self, tagname, href, lnum):
        if self.current_name:
            c = current_container()
            if tagname == 'a' and href:
                if href and href.startswith('#'):
                    name = self.current_name
                else:
                    name = c.href_to_name(href,
                                          self.current_name) if href else None
                if name == self.current_name:
                    return self.view.page().go_to_anchor(
                        urlparse(href).fragment, lnum)
                if name and c.exists(name) and c.mime_map[name] in OEB_DOCS:
                    return self.link_clicked.emit(
                        name,
                        urlparse(href).fragment or TOP)
            self.sync_requested.emit(self.current_name, lnum)

    def request_split(self, loc, totals):
        if self.current_name:
            self.split_requested.emit(self.current_name, loc, totals)

    def sync_to_editor(self, name, sourceline_address):
        self.current_sync_request = (name, sourceline_address)
        QTimer.singleShot(100, self._sync_to_editor)

    def _sync_to_editor(self):
        if not actions['sync-preview-to-editor'].isChecked():
            return
        try:
            if self.refresh_timer.isActive(
            ) or self.current_sync_request[0] != self.current_name:
                return QTimer.singleShot(100, self._sync_to_editor)
        except TypeError:
            return  # Happens if current_sync_request is None
        sourceline_address = self.current_sync_request[1]
        self.current_sync_request = None
        self.view.page().go_to_sourceline_address(sourceline_address)

    def report_worker_launch_error(self):
        if parse_worker.launch_error is not None:
            tb, parse_worker.launch_error = parse_worker.launch_error, None
            error_dialog(
                self,
                _('Failed to launch worker'),
                _('Failed to launch the worker process used for rendering the preview'
                  ),
                det_msg=tb,
                show=True)

    def show(self, name):
        if name != self.current_name:
            self.refresh_timer.stop()
            self.current_name = name
            self.report_worker_launch_error()
            parse_worker.add_request(name)
            self.view.setUrl(
                QUrl.fromLocalFile(current_container().name_to_abspath(name)))
            return True

    def refresh(self):
        if self.current_name:
            self.refresh_timer.stop()
            # This will check if the current html has changed in its editor,
            # and re-parse it if so
            self.report_worker_launch_error()
            parse_worker.add_request(self.current_name)
            # Tell webkit to reload all html and associated resources
            current_url = QUrl.fromLocalFile(
                current_container().name_to_abspath(self.current_name))
            self.refresh_starting.emit()
            if current_url != self.view.url():
                # The container was changed
                self.view.setUrl(current_url)
            else:
                self.view.refresh()
            self.refreshed.emit()

    def clear(self):
        self.view.clear()
        self.current_name = None

    @property
    def current_root(self):
        return self.view.page().current_root

    @property
    def is_visible(self):
        return actions['preview-dock'].isChecked()

    @property
    def live_css_is_visible(self):
        try:
            return actions['live-css-dock'].isChecked()
        except KeyError:
            return False

    def start_refresh_timer(self):
        if self.live_css_is_visible or (
                self.is_visible
                and actions['auto-reload-preview'].isChecked()):
            self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000)

    def stop_refresh_timer(self):
        self.refresh_timer.stop()

    def auto_reload_toggled(self, checked):
        if self.live_css_is_visible and not actions[
                'auto-reload-preview'].isChecked():
            actions['auto-reload-preview'].setChecked(True)
            error_dialog(
                self,
                _('Cannot disable'),
                _('Auto reloading of the preview panel cannot be disabled while the'
                  ' Live CSS panel is open.'),
                show=True)
        actions['auto-reload-preview'].setToolTip(
            _('Auto reload preview when text changes in editor'
              ) if not checked else _('Disable auto reload of preview'))

    def sync_toggled(self, checked):
        actions['sync-preview-to-editor'].setToolTip(
            _('Disable syncing of preview position to editor position'
              ) if checked else _(
                  'Enable syncing of preview position to editor position'))

    def visibility_changed(self, is_visible):
        if is_visible:
            self.refresh()

    def split_toggled(self, checked):
        actions['split-in-preview'].setToolTip(
            textwrap.fill(
                _('Abort file split') if checked else
                _('Split this file at a specified location.\n\nAfter clicking this button, click'
                  ' inside the preview panel above at the location you want the file to be split.'
                  )))
        if checked:
            self.split_start_requested.emit()
        else:
            self.view.page().split_mode(False)

    def do_start_split(self):
        self.view.page().split_mode(True)

    def stop_split(self):
        actions['split-in-preview'].setChecked(False)

    def load_finished(self, ok):
        if actions['split-in-preview'].isChecked():
            if ok:
                self.do_start_split()
            else:
                self.stop_split()

    def apply_settings(self):
        s = self.view.page().settings()
        s.setFontSize(s.DefaultFontSize, tprefs['preview_base_font_size'])
        s.setFontSize(s.DefaultFixedFontSize, tprefs['preview_mono_font_size'])
        s.setFontSize(s.MinimumLogicalFontSize,
                      tprefs['preview_minimum_font_size'])
        s.setFontSize(s.MinimumFontSize, tprefs['preview_minimum_font_size'])
        sf, ssf, mf = tprefs['preview_serif_family'], tprefs[
            'preview_sans_family'], tprefs['preview_mono_family']
        s.setFontFamily(s.StandardFont, {
            'serif': sf,
            'sans': ssf,
            'mono': mf,
            None: sf
        }[tprefs['preview_standard_font_family']])
        s.setFontFamily(s.SerifFont, sf)
        s.setFontFamily(s.SansSerifFont, ssf)
        s.setFontFamily(s.FixedFont, mf)
示例#14
0
class WebPage(QWebPage):

    sync_requested = pyqtSignal(object, object, object)
    split_requested = pyqtSignal(object, object)

    def __init__(self, parent):
        QWebPage.__init__(self, parent)
        settings = self.settings()
        apply_settings(settings, config().parse())
        settings.setMaximumPagesInCache(0)
        settings.setAttribute(settings.JavaEnabled, False)
        settings.setAttribute(settings.PluginsEnabled, False)
        settings.setAttribute(settings.PrivateBrowsingEnabled, True)
        settings.setAttribute(settings.JavascriptCanOpenWindows, False)
        settings.setAttribute(settings.JavascriptCanAccessClipboard, False)
        settings.setAttribute(settings.LinksIncludedInFocusChain, False)
        settings.setAttribute(settings.DeveloperExtrasEnabled, True)
        settings.setDefaultTextEncoding('utf-8')
        data = 'data:text/css;charset=utf-8;base64,'
        css = '[data-in-split-mode="1"] [data-is-block="1"]:hover { cursor: pointer !important; border-top: solid 5px green !important }'
        data += b64encode(css.encode('utf-8'))
        settings.setUserStyleSheetUrl(QUrl(data))

        self.setNetworkAccessManager(NetworkAccessManager(self))
        self.setLinkDelegationPolicy(self.DelegateAllLinks)
        self.mainFrame().javaScriptWindowObjectCleared.connect(
            self.init_javascript)
        self.init_javascript()

    @dynamic_property
    def current_root(self):
        def fget(self):
            return self.networkAccessManager().current_root

        def fset(self, val):
            self.networkAccessManager().current_root = val

        return property(fget=fget, fset=fset)

    def javaScriptConsoleMessage(self, msg, lineno, source_id):
        prints('preview js:%s:%s:' % (unicode(source_id), lineno),
               unicode(msg))

    def init_javascript(self):
        if not hasattr(self, 'js'):
            from calibre.utils.resources import compiled_coffeescript
            self.js = compiled_coffeescript('ebooks.oeb.display.utils',
                                            dynamic=False)
            self.js += P('csscolorparser.js',
                         data=True,
                         allow_user_override=False)
            self.js += compiled_coffeescript('ebooks.oeb.polish.preview',
                                             dynamic=False)
        self._line_numbers = None
        mf = self.mainFrame()
        mf.addToJavaScriptWindowObject("py_bridge", self)
        mf.evaluateJavaScript(self.js)

    @pyqtSlot(str, str, str)
    def request_sync(self, tag_name, href, sourceline_address):
        try:
            self.sync_requested.emit(unicode(tag_name), unicode(href),
                                     json.loads(unicode(sourceline_address)))
        except (TypeError, ValueError, OverflowError, AttributeError):
            pass

    def go_to_anchor(self, anchor, lnum):
        self.mainFrame().evaluateJavaScript(
            'window.calibre_preview_integration.go_to_anchor(%s, %s)' %
            (json.dumps(anchor), json.dumps(str(lnum))))

    @pyqtSlot(str, str)
    def request_split(self, loc, totals):
        actions['split-in-preview'].setChecked(False)
        loc, totals = json.loads(unicode(loc)), json.loads(unicode(totals))
        if not loc or not totals:
            return error_dialog(self.view(),
                                _('Invalid location'),
                                _('Cannot split on the body tag'),
                                show=True)
        self.split_requested.emit(loc, totals)

    @property
    def line_numbers(self):
        if self._line_numbers is None:

            def atoi(x):
                try:
                    ans = int(x)
                except (TypeError, ValueError):
                    ans = None
                return ans

            val = self.mainFrame().evaluateJavaScript(
                'window.calibre_preview_integration.line_numbers()')
            self._line_numbers = sorted(
                uniq(filter(lambda x: x is not None, map(atoi, val))))
        return self._line_numbers

    def go_to_line(self, lnum):
        try:
            lnum = find_le(self.line_numbers, lnum)
        except IndexError:
            return
        self.mainFrame().evaluateJavaScript(
            'window.calibre_preview_integration.go_to_line(%d)' % lnum)

    def go_to_sourceline_address(self, sourceline_address):
        lnum, tags = sourceline_address
        if lnum is None:
            return
        tags = [x.lower() for x in tags]
        self.mainFrame().evaluateJavaScript(
            'window.calibre_preview_integration.go_to_sourceline_address(%d, %s)'
            % (lnum, json.dumps(tags)))

    def split_mode(self, enabled):
        self.mainFrame().evaluateJavaScript(
            'window.calibre_preview_integration.split_mode(%s)' %
            ('true' if enabled else 'false'))
示例#15
0
class FileList(QTreeWidget):

    delete_requested = pyqtSignal(object, object)
    reorder_spine = pyqtSignal(object)
    rename_requested = pyqtSignal(object, object)
    bulk_rename_requested = pyqtSignal(object)
    edit_file = pyqtSignal(object, object, object)
    merge_requested = pyqtSignal(object, object, object)
    mark_requested = pyqtSignal(object, object)
    export_requested = pyqtSignal(object, object)
    replace_requested = pyqtSignal(object, object, object, object)
    link_stylesheets_requested = pyqtSignal(object, object, object)
    initiate_file_copy = pyqtSignal(object)
    initiate_file_paste = pyqtSignal()

    def __init__(self, parent=None):
        QTreeWidget.__init__(self, parent)
        self.categories = {}
        self.ordered_selected_indexes = False
        pi = plugins['progress_indicator'][0]
        if hasattr(pi, 'set_no_activate_on_click'):
            pi.set_no_activate_on_click(self)
        self.current_edited_name = None
        self.delegate = ItemDelegate(self)
        self.delegate.rename_requested.connect(self.rename_requested)
        self.setTextElideMode(Qt.ElideMiddle)
        self.setItemDelegate(self.delegate)
        self.setIconSize(QSize(16, 16))
        self.header().close()
        self.setDragEnabled(True)
        self.setEditTriggers(self.EditKeyPressed)
        self.setSelectionMode(self.ExtendedSelection)
        self.viewport().setAcceptDrops(True)
        self.setDropIndicatorShown(True)
        self.setDragDropMode(self.InternalMove)
        self.setAutoScroll(True)
        self.setAutoScrollMargin(TOP_ICON_SIZE * 2)
        self.setDefaultDropAction(Qt.MoveAction)
        self.setAutoExpandDelay(1000)
        self.setAnimated(True)
        self.setMouseTracking(True)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.root = self.invisibleRootItem()
        self.emblem_cache = {}
        self.rendered_emblem_cache = {}
        self.top_level_pixmap_cache = {
            name: QIcon(I(icon)).pixmap(TOP_ICON_SIZE, TOP_ICON_SIZE)
            for name, icon in iteritems({
                'text': 'keyboard-prefs.png',
                'styles': 'lookfeel.png',
                'fonts': 'font.png',
                'misc': 'mimetypes/dir.png',
                'images': 'view-image.png',
            })
        }
        self.itemActivated.connect(self.item_double_clicked)

    def mimeTypes(self):
        ans = QTreeWidget.mimeTypes(self)
        ans.append(CONTAINER_DND_MIMETYPE)
        return ans

    def mimeData(self, indices):
        ans = QTreeWidget.mimeData(self, indices)
        names = (idx.data(0, NAME_ROLE) for idx in indices
                 if idx.data(0, MIME_ROLE))
        ans.setData(CONTAINER_DND_MIMETYPE,
                    '\n'.join(filter(None, names)).encode('utf-8'))
        return ans

    @property
    def current_name(self):
        ci = self.currentItem()
        if ci is not None:
            return unicode_type(ci.data(0, NAME_ROLE) or '')
        return ''

    def get_state(self):
        s = {'pos': self.verticalScrollBar().value()}
        s['expanded'] = {
            c
            for c, item in iteritems(self.categories) if item.isExpanded()
        }
        s['selected'] = {
            unicode_type(i.data(0, NAME_ROLE) or '')
            for i in self.selectedItems()
        }
        return s

    def set_state(self, state):
        for category, item in iteritems(self.categories):
            item.setExpanded(category in state['expanded'])
        self.verticalScrollBar().setValue(state['pos'])
        for parent in itervalues(self.categories):
            for c in (parent.child(i) for i in range(parent.childCount())):
                name = unicode_type(c.data(0, NAME_ROLE) or '')
                if name in state['selected']:
                    c.setSelected(True)

    def item_from_name(self, name):
        for parent in itervalues(self.categories):
            for c in (parent.child(i) for i in range(parent.childCount())):
                q = unicode_type(c.data(0, NAME_ROLE) or '')
                if q == name:
                    return c

    def select_name(self, name, set_as_current_index=False):
        for parent in itervalues(self.categories):
            for c in (parent.child(i) for i in range(parent.childCount())):
                q = unicode_type(c.data(0, NAME_ROLE) or '')
                c.setSelected(q == name)
                if q == name:
                    self.scrollToItem(c)
                    if set_as_current_index:
                        self.setCurrentItem(c)

    def select_names(self, names, current_name=None):
        for parent in itervalues(self.categories):
            for c in (parent.child(i) for i in range(parent.childCount())):
                q = unicode_type(c.data(0, NAME_ROLE) or '')
                c.setSelected(q in names)
                if q == current_name:
                    self.scrollToItem(c)
                    s = self.selectionModel()
                    s.setCurrentIndex(self.indexFromItem(c), s.NoUpdate)

    def mark_name_as_current(self, name):
        current = self.item_from_name(name)
        if current is not None:
            if self.current_edited_name is not None:
                ci = self.item_from_name(self.current_edited_name)
                if ci is not None:
                    ci.setData(0, Qt.FontRole, None)
            self.current_edited_name = name
            self.mark_item_as_current(current)

    def mark_item_as_current(self, item):
        font = QFont(self.font())
        font.setItalic(True)
        font.setBold(True)
        item.setData(0, Qt.FontRole, font)

    def clear_currently_edited_name(self):
        if self.current_edited_name:
            ci = self.item_from_name(self.current_edited_name)
            if ci is not None:
                ci.setData(0, Qt.FontRole, None)
        self.current_edited_name = None

    def build(self, container, preserve_state=True):
        if container is None:
            return
        if preserve_state:
            state = self.get_state()
        self.clear()
        self.root = self.invisibleRootItem()
        self.root.setFlags(Qt.ItemIsDragEnabled)
        self.categories = {}
        for category, text, __ in CATEGORIES:
            self.categories[category] = i = QTreeWidgetItem(self.root, 0)
            i.setText(0, text)
            i.setData(0, Qt.DecorationRole,
                      self.top_level_pixmap_cache[category])
            f = i.font(0)
            f.setBold(True)
            i.setFont(0, f)
            i.setData(0, NAME_ROLE, category)
            flags = Qt.ItemIsEnabled
            if category == 'text':
                flags |= Qt.ItemIsDropEnabled
            i.setFlags(flags)

        processed, seen = {}, {}

        cover_page_name = get_cover_page_name(container)
        cover_image_name = get_raster_cover_name(container)
        manifested_names = set()
        for names in itervalues(container.manifest_type_map):
            manifested_names |= set(names)

        def get_category(name, mt):
            category = 'misc'
            if mt.startswith('image/'):
                category = 'images'
            elif mt in OEB_FONTS:
                category = 'fonts'
            elif mt in OEB_STYLES:
                category = 'styles'
            elif mt in OEB_DOCS:
                category = 'text'
            ext = name.rpartition('.')[-1].lower()
            if ext in {'ttf', 'otf', 'woff'}:
                # Probably wrong mimetype in the OPF
                category = 'fonts'
            return category

        def set_display_name(name, item):
            if tprefs['file_list_shows_full_pathname']:
                text = name
            else:
                if name in processed:
                    # We have an exact duplicate (can happen if there are
                    # duplicates in the spine)
                    item.setText(0, processed[name].text(0))
                    item.setText(1, processed[name].text(1))
                    return

                parts = name.split('/')
                text = parts.pop()
                while text in seen and parts:
                    text = parts.pop() + '/' + text

            seen[text] = item
            item.setText(0, text)
            item.setText(1, as_hex_unicode(numeric_sort_key(text)))

        def render_emblems(item, emblems):
            emblems = tuple(emblems)
            if not emblems:
                return
            icon = self.rendered_emblem_cache.get(emblems, None)
            if icon is None:
                pixmaps = []
                for emblem in emblems:
                    pm = self.emblem_cache.get(emblem, None)
                    if pm is None:
                        pm = self.emblem_cache[emblem] = QIcon(
                            I(emblem)).pixmap(self.iconSize())
                    pixmaps.append(pm)
                num = len(pixmaps)
                w, h = pixmaps[0].width(), pixmaps[0].height()
                if num == 1:
                    icon = self.rendered_emblem_cache[emblems] = QIcon(
                        pixmaps[0])
                else:
                    canvas = QPixmap((num * w) + ((num - 1) * 2), h)
                    canvas.setDevicePixelRatio(pixmaps[0].devicePixelRatio())
                    canvas.fill(Qt.transparent)
                    painter = QPainter(canvas)
                    for i, pm in enumerate(pixmaps):
                        painter.drawPixmap(
                            int(i * (w + 2) / canvas.devicePixelRatio()), 0,
                            pm)
                    painter.end()
                    icon = self.rendered_emblem_cache[emblems] = canvas
            item.setData(0, Qt.DecorationRole, icon)

        cannot_be_renamed = container.names_that_must_not_be_changed
        ncx_mime = guess_type('a.ncx')
        nav_items = frozenset(container.manifest_items_with_property('nav'))

        def create_item(name, linear=None):
            imt = container.mime_map.get(name, guess_type(name))
            icat = get_category(name, imt)
            category = 'text' if linear is not None else ({
                'text': 'misc'
            }.get(icat, icat))
            item = QTreeWidgetItem(
                self.categories['text' if linear is not None else category], 1)
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            if category == 'text':
                flags |= Qt.ItemIsDragEnabled
            if name not in cannot_be_renamed:
                flags |= Qt.ItemIsEditable
            item.setFlags(flags)
            item.setStatusTip(0, _('Full path: ') + name)
            item.setData(0, NAME_ROLE, name)
            item.setData(0, CATEGORY_ROLE, category)
            item.setData(0, LINEAR_ROLE, bool(linear))
            item.setData(0, MIME_ROLE, imt)
            set_display_name(name, item)
            tooltips = []
            emblems = []
            if name in {cover_page_name, cover_image_name}:
                emblems.append('default_cover.png')
                tooltips.append(
                    _('This file is the cover %s for this book') %
                    (_('image') if name == cover_image_name else _('page')))
            if name in container.opf_name:
                emblems.append('metadata.png')
                tooltips.append(
                    _('This file contains all the metadata and book structure information'
                      ))
            if imt == ncx_mime or name in nav_items:
                emblems.append('toc.png')
                tooltips.append(
                    _('This file contains the metadata table of contents'))
            if name not in manifested_names and not container.ok_to_be_unmanifested(
                    name):
                emblems.append('dialog_question.png')
                tooltips.append(
                    _('This file is not listed in the book manifest'))
            if linear is False:
                emblems.append('arrow-down.png')
                tooltips.append(
                    _('This file is marked as non-linear in the spine\nDrag it to the top to make it linear'
                      ))
            if linear is None and icat == 'text':
                # Text item outside spine
                emblems.append('dialog_warning.png')
                tooltips.append(
                    _('This file is a text file that is not referenced in the spine'
                      ))
            if category == 'text' and name in processed:
                # Duplicate entry in spine
                emblems.append('dialog_error.png')
                tooltips.append(
                    _('This file occurs more than once in the spine'))

            render_emblems(item, emblems)
            if tooltips:
                item.setData(0, Qt.ToolTipRole, '\n'.join(tooltips))
            return item

        for name, linear in container.spine_names:
            processed[name] = create_item(name, linear=linear)

        for name in container.name_path_map:
            if name in processed:
                continue
            processed[name] = create_item(name)

        for name, c in iteritems(self.categories):
            c.setExpanded(True)
            if name != 'text':
                c.sortChildren(1, Qt.AscendingOrder)

        if preserve_state:
            self.set_state(state)

        if self.current_edited_name:
            item = self.item_from_name(self.current_edited_name)
            if item is not None:
                self.mark_item_as_current(item)

    def show_context_menu(self, point):
        item = self.itemAt(point)
        if item is None or item in tuple(itervalues(self.categories)):
            return
        m = QMenu(self)
        sel = self.selectedItems()
        num = len(sel)
        container = current_container()
        ci = self.currentItem()
        if ci is not None:
            cn = unicode_type(ci.data(0, NAME_ROLE) or '')
            mt = unicode_type(ci.data(0, MIME_ROLE) or '')
            cat = unicode_type(ci.data(0, CATEGORY_ROLE) or '')
            n = elided_text(cn.rpartition('/')[-1])
            m.addAction(QIcon(I('save.png')),
                        _('Export %s') % n, partial(self.export, cn))
            if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS:
                m.addAction(
                    _('Replace %s with file...') % n,
                    partial(self.replace, cn))
            if num > 1:
                m.addAction(QIcon(I('save.png')),
                            _('Export all %d selected files') % num,
                            self.export_selected)

            m.addSeparator()

            m.addAction(QIcon(I('modified.png')),
                        _('&Rename %s') % n, self.edit_current_item)
            if is_raster_image(mt):
                m.addAction(QIcon(I('default_cover.png')),
                            _('Mark %s as cover image') % n,
                            partial(self.mark_as_cover, cn))
            elif current_container(
            ).SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text':
                m.addAction(QIcon(I('default_cover.png')),
                            _('Mark %s as cover page') % n,
                            partial(self.mark_as_titlepage, cn))
            m.addSeparator()

        if num > 0:
            m.addSeparator()
            if num > 1:
                m.addAction(QIcon(I('modified.png')),
                            _('&Bulk rename the selected files'),
                            self.request_bulk_rename)
            m.addAction(QIcon(I('modified.png')),
                        _('Change the file extension for the selected files'),
                        self.request_change_ext)
            m.addAction(
                QIcon(I('trash.png')),
                ngettext('&Delete the selected file',
                         '&Delete the {} selected files', num).format(num),
                self.request_delete)
            m.addAction(
                QIcon(I('edit-copy.png')),
                ngettext(
                    '&Copy the selected file to another editor instance',
                    '&Copy the {} selected files to another editor instance',
                    num).format(num), self.copy_selected_files)
            m.addSeparator()
        md = QApplication.instance().clipboard().mimeData()
        if md.hasUrls() and md.hasFormat(FILE_COPY_MIME):
            m.addAction(_('Paste files from other editor instance'),
                        self.paste_from_other_instance)

        selected_map = defaultdict(list)
        for item in sel:
            selected_map[unicode_type(
                item.data(0, CATEGORY_ROLE)
                or '')].append(unicode_type(item.data(0, NAME_ROLE) or ''))

        for items in itervalues(selected_map):
            items.sort(key=self.index_of_name)

        if selected_map['text']:
            m.addAction(QIcon(I('format-text-color.png')),
                        _('Link &stylesheets...'),
                        partial(self.link_stylesheets, selected_map['text']))

        if len(selected_map['text']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected text files'),
                partial(self.start_merge, 'text', selected_map['text']))
        if len(selected_map['styles']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected style files'),
                partial(self.start_merge, 'styles', selected_map['styles']))

        if len(list(m.actions())) > 0:
            m.popup(self.mapToGlobal(point))

    def index_of_name(self, name):
        for category, parent in iteritems(self.categories):
            for i in range(parent.childCount()):
                item = parent.child(i)
                if unicode_type(item.data(0, NAME_ROLE) or '') == name:
                    return (category, i)
        return (None, -1)

    def start_merge(self, category, names):
        d = MergeDialog(names, self)
        if d.exec_() == d.Accepted and d.ans:
            self.merge_requested.emit(category, names, d.ans)

    def edit_current_item(self):
        if not current_container().SUPPORTS_FILENAMES:
            error_dialog(
                self,
                _('Cannot rename'),
                _('%s books do not support file renaming as they do not use file names'
                  ' internally. The filenames you see are automatically generated from the'
                  ' internal structures of the original file.') %
                current_container().book_type.upper(),
                show=True)
            return
        if self.currentItem() is not None:
            self.editItem(self.currentItem())

    def mark_as_cover(self, name):
        self.mark_requested.emit(name, 'cover')

    def mark_as_titlepage(self, name):
        first = unicode_type(
            self.categories['text'].child(0).data(0, NAME_ROLE) or '') == name
        move_to_start = False
        if not first:
            move_to_start = question_dialog(
                self, _('Not first item'),
                _('%s is not the first text item. You should only mark the'
                  ' first text item as cover. Do you want to make it the'
                  ' first item?') % elided_text(name))
        self.mark_requested.emit(name, 'titlepage:%r' % move_to_start)

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
            ev.accept()
            self.request_delete()
        else:
            return QTreeWidget.keyPressEvent(self, ev)

    def request_rename_common(self):
        if not current_container().SUPPORTS_FILENAMES:
            error_dialog(
                self,
                _('Cannot rename'),
                _('%s books do not support file renaming as they do not use file names'
                  ' internally. The filenames you see are automatically generated from the'
                  ' internal structures of the original file.') %
                current_container().book_type.upper(),
                show=True)
            return
        names = {
            unicode_type(item.data(0, NAME_ROLE) or '')
            for item in self.selectedItems()
        }
        bad = names & current_container().names_that_must_not_be_changed
        if bad:
            error_dialog(self,
                         _('Cannot rename'),
                         _('The file(s) %s cannot be renamed.') %
                         ('<b>%s</b>' % ', '.join(bad)),
                         show=True)
            return
        names = sorted(names, key=self.index_of_name)
        return names

    def request_bulk_rename(self):
        names = self.request_rename_common()
        if names is not None:
            categories = Counter(
                unicode_type(item.data(0, CATEGORY_ROLE) or '')
                for item in self.selectedItems())
            settings = get_bulk_rename_settings(
                self,
                len(names),
                category=categories.most_common(1)[0][0],
                allow_spine_order=True)
            fmt, num = settings['prefix'], settings['start']
            if fmt is not None:

                def change_name(name, num):
                    parts = name.split('/')
                    base, ext = parts[-1].rpartition('.')[0::2]
                    parts[-1] = (fmt % num) + '.' + ext
                    return '/'.join(parts)

                if settings['spine_order']:
                    order_map = get_spine_order_for_all_files(
                        current_container())
                    select_map = {n: i for i, n in enumerate(names)}

                    def key(n):
                        return order_map.get(n, (sys.maxsize, select_map[n]))

                    name_map = {
                        n: change_name(n, num + i)
                        for i, n in enumerate(sorted(names, key=key))
                    }
                else:
                    name_map = {
                        n: change_name(n, num + i)
                        for i, n in enumerate(names)
                    }
                self.bulk_rename_requested.emit(name_map)

    def request_change_ext(self):
        names = self.request_rename_common()
        if names is not None:
            text, ok = QInputDialog.getText(self, _('Rename files'),
                                            _('New file extension:'))
            if ok and text:
                ext = text.lstrip('.')

                def change_name(name):
                    base = posixpath.splitext(name)[0]
                    return base + '.' + ext

                name_map = {n: change_name(n) for n in names}
                self.bulk_rename_requested.emit(name_map)

    @property
    def selected_names(self):
        ans = {
            unicode_type(item.data(0, NAME_ROLE) or '')
            for item in self.selectedItems()
        }
        ans.discard('')
        return ans

    def copy_selected_files(self):
        self.initiate_file_copy.emit(self.selected_names)

    def paste_from_other_instance(self):
        self.initiate_file_paste.emit()

    def request_delete(self):
        names = self.selected_names
        bad = names & current_container().names_that_must_not_be_removed
        if bad:
            return error_dialog(self,
                                _('Cannot delete'),
                                _('The file(s) %s cannot be deleted.') %
                                ('<b>%s</b>' % ', '.join(bad)),
                                show=True)

        text = self.categories['text']
        children = (text.child(i) for i in range(text.childCount()))
        spine_removals = [(unicode_type(item.data(0, NAME_ROLE)
                                        or ''), item.isSelected())
                          for item in children]
        other_removals = {
            unicode_type(item.data(0, NAME_ROLE) or '')
            for item in self.selectedItems()
            if unicode_type(item.data(0, CATEGORY_ROLE) or '') != 'text'
        }
        self.delete_requested.emit(spine_removals, other_removals)

    def delete_done(self, spine_removals, other_removals):
        removals = []
        for i, (name, remove) in enumerate(spine_removals):
            if remove:
                removals.append(self.categories['text'].child(i))
        for category, parent in iteritems(self.categories):
            if category != 'text':
                for i in range(parent.childCount()):
                    child = parent.child(i)
                    if unicode_type(child.data(0, NAME_ROLE)
                                    or '') in other_removals:
                        removals.append(child)

        # The sorting by index is necessary otherwise Qt crashes with recursive
        # repaint detected message
        for c in sorted(removals,
                        key=lambda x: x.parent().indexOfChild(x),
                        reverse=True):
            sip.delete(c)

        # A bug in the raster paint engine on linux causes a crash if the scrollbar
        # is at the bottom and the delete happens to cause the scrollbar to
        # update
        b = self.verticalScrollBar()
        if b.value() == b.maximum():
            b.setValue(b.minimum())
            QTimer.singleShot(0, lambda: b.setValue(b.maximum()))

    def __enter__(self):
        self.ordered_selected_indexes = True

    def __exit__(self, *args):
        self.ordered_selected_indexes = False

    def selectedIndexes(self):
        ans = QTreeWidget.selectedIndexes(self)
        if self.ordered_selected_indexes:
            ans = list(sorted(ans, key=lambda idx: idx.row()))
        return ans

    def dropEvent(self, event):
        with self:
            text = self.categories['text']
            pre_drop_order = {
                text.child(i): i
                for i in range(text.childCount())
            }
            super(FileList, self).dropEvent(event)
            current_order = {
                text.child(i): i
                for i in range(text.childCount())
            }
            if current_order != pre_drop_order:
                order = []
                for child in (text.child(i) for i in range(text.childCount())):
                    name = unicode_type(child.data(0, NAME_ROLE) or '')
                    linear = bool(child.data(0, LINEAR_ROLE))
                    order.append([name, linear])
                # Ensure that all non-linear items are at the end, any non-linear
                # items not at the end will be made linear
                for i, (name, linear) in tuple(enumerate(order)):
                    if not linear and i < len(order) - 1 and order[i + 1][1]:
                        order[i][1] = True
                self.reorder_spine.emit(order)

    def item_double_clicked(self, item, column):
        category = unicode_type(item.data(0, CATEGORY_ROLE) or '')
        if category:
            self._request_edit(item)

    def _request_edit(self, item):
        category = unicode_type(item.data(0, CATEGORY_ROLE) or '')
        mime = unicode_type(item.data(0, MIME_ROLE) or '')
        name = unicode_type(item.data(0, NAME_ROLE) or '')
        syntax = {'text': 'html', 'styles': 'css'}.get(category, None)
        self.edit_file.emit(name, syntax, mime)

    def request_edit(self, name):
        item = self.item_from_name(name)
        if item is not None:
            self._request_edit(item)
        else:
            error_dialog(self,
                         _('Cannot edit'),
                         _('No item with the name: %s was found') % name,
                         show=True)

    def edit_next_file(self, currently_editing=None, backwards=False):
        category = self.categories['text']
        seen_current = False
        items = (category.child(i) for i in range(category.childCount()))
        if backwards:
            items = reversed(tuple(items))
        for item in items:
            name = unicode_type(item.data(0, NAME_ROLE) or '')
            if seen_current:
                self._request_edit(item)
                return True
            if currently_editing == name:
                seen_current = True
        return False

    @property
    def all_files(self):
        return (category.child(i) for category in itervalues(self.categories)
                for i in range(category.childCount()))

    @property
    def searchable_names(self):
        ans = {
            'text': OrderedDict(),
            'styles': OrderedDict(),
            'selected': OrderedDict(),
            'open': OrderedDict()
        }
        for item in self.all_files:
            category = unicode_type(item.data(0, CATEGORY_ROLE) or '')
            mime = unicode_type(item.data(0, MIME_ROLE) or '')
            name = unicode_type(item.data(0, NAME_ROLE) or '')
            ok = category in {'text', 'styles'}
            if ok:
                ans[category][name] = syntax_from_mime(name, mime)
            if not ok:
                if category == 'misc':
                    ok = mime in {
                        guess_type('a.' + x)
                        for x in ('opf', 'ncx', 'txt', 'xml')
                    }
                elif category == 'images':
                    ok = mime == guess_type('a.svg')
            if ok:
                cats = []
                if item.isSelected():
                    cats.append('selected')
                if name in editors:
                    cats.append('open')
                for cat in cats:
                    ans[cat][name] = syntax_from_mime(name, mime)
        return ans

    def export(self, name):
        path = choose_save_file(self,
                                'tweak_book_export_file',
                                _('Choose location'),
                                filters=[(_('Files'),
                                          [name.rpartition('.')[-1].lower()])],
                                all_files=False,
                                initial_filename=name.split('/')[-1])
        if path:
            self.export_requested.emit(name, path)

    def export_selected(self):
        names = self.selected_names
        if not names:
            return
        path = choose_dir(self, 'tweak_book_export_selected',
                          _('Choose location'))
        if path:
            self.export_requested.emit(names, path)

    def replace(self, name):
        c = current_container()
        mt = c.mime_map[name]
        oext = name.rpartition('.')[-1].lower()
        filters = [oext]
        fname = _('Files')
        if mt in OEB_DOCS:
            fname = _('HTML files')
            filters = 'html htm xhtm xhtml shtml'.split()
        elif is_raster_image(mt):
            fname = _('Images')
            filters = 'jpeg jpg gif png'.split()
        path = choose_files(self,
                            'tweak_book_import_file',
                            _('Choose file'),
                            filters=[(fname, filters)],
                            select_only_single_file=True)
        if not path:
            return
        path = path[0]
        ext = path.rpartition('.')[-1].lower()
        force_mt = None
        if mt in OEB_DOCS:
            force_mt = c.guess_type('a.html')
        nname = os.path.basename(path)
        nname, ext = nname.rpartition('.')[0::2]
        nname = nname + '.' + ext.lower()
        self.replace_requested.emit(name, path, nname, force_mt)

    def link_stylesheets(self, names):
        s = self.categories['styles']
        sheets = [
            unicode_type(s.child(i).data(0, NAME_ROLE) or '')
            for i in range(s.childCount())
        ]
        if not sheets:
            return error_dialog(
                self,
                _('No stylesheets'),
                _('This book currently has no stylesheets. You must first create a stylesheet'
                  ' before linking it.'),
                show=True)
        d = QDialog(self)
        d.l = l = QVBoxLayout(d)
        d.setLayout(l)
        d.setWindowTitle(_('Choose stylesheets'))
        d.la = la = QLabel(
            _('Choose the stylesheets to link. Drag and drop to re-arrange'))

        la.setWordWrap(True)
        l.addWidget(la)
        d.s = s = QListWidget(d)
        l.addWidget(s)
        s.setDragEnabled(True)
        s.setDropIndicatorShown(True)
        s.setDragDropMode(self.InternalMove)
        s.setAutoScroll(True)
        s.setDefaultDropAction(Qt.MoveAction)
        for name in sheets:
            i = QListWidgetItem(name, s)
            flags = Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsDragEnabled | Qt.ItemIsSelectable
            i.setFlags(flags)
            i.setCheckState(Qt.Checked)
        d.r = r = QCheckBox(_('Remove existing links to stylesheets'))
        r.setChecked(tprefs['remove_existing_links_when_linking_sheets'])
        l.addWidget(r)
        d.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok
                                     | QDialogButtonBox.Cancel)
        bb.accepted.connect(d.accept), bb.rejected.connect(d.reject)
        l.addWidget(bb)
        if d.exec_() == d.Accepted:
            tprefs['remove_existing_links_when_linking_sheets'] = r.isChecked()
            sheets = [
                unicode_type(s.item(il).text()) for il in range(s.count())
                if s.item(il).checkState() == Qt.Checked
            ]
            if sheets:
                self.link_stylesheets_requested.emit(names, sheets,
                                                     r.isChecked())
示例#16
0
class ChooseLibraryAction(InterfaceAction):

    name = 'Choose Library'
    action_spec = (_('Choose Library'), 'lt.png',
            _('Choose calibre library to work with'), None)
    dont_add_to = frozenset(['context-menu-device'])
    action_add_menu = True
    action_menu_clone_qaction = _('Switch/create library...')
    restore_view_state = pyqtSignal(object)

    def genesis(self):
        self.count_changed(0)
        self.action_choose = self.menuless_qaction
        self.action_exim = ac = QAction(_('Export/Import all calibre data'), self.gui)
        ac.triggered.connect(self.exim_data)

        self.stats = LibraryUsageStats()
        self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else
                QToolButton.MenuButtonPopup)
        if len(self.stats.stats) > 1:
            self.action_choose.triggered.connect(self.choose_library)
        else:
            self.qaction.triggered.connect(self.choose_library)

        self.choose_menu = self.qaction.menu()

        ac = self.create_action(spec=(_('Pick a random book'), 'random.png',
            None, None), attr='action_pick_random')
        ac.triggered.connect(self.pick_random)

        if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            self.choose_menu.addAction(self.action_choose)

            self.quick_menu = QMenu(_('Quick switch'))
            self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
            self.rename_menu = QMenu(_('Rename library'))
            self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu)
            self.choose_menu.addAction(ac)
            self.delete_menu = QMenu(_('Remove library'))
            self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
            self.choose_menu.addAction(self.action_exim)
        else:
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.switch_actions = []
        for i in range(5):
            ac = self.create_action(spec=('', None, None, None),
                    attr='switch_action%d'%i)
            self.switch_actions.append(ac)
            ac.setVisible(False)
            ac.triggered.connect(partial(self.qs_requested, i),
                    type=Qt.QueuedConnection)
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.maintenance_menu = QMenu(_('Library maintenance'))
        ac = self.create_action(spec=(_('Library metadata backup status'),
                        'lt.png', None, None), attr='action_backup_status')
        ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Check library'), 'lt.png',
                                      None, None), attr='action_check_library')
        ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Restore database'), 'lt.png',
                                      None, None),
                                      attr='action_restore_database')
        ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)

        self.choose_menu.addMenu(self.maintenance_menu)
        self.view_state_map = {}
        self.restore_view_state.connect(self._restore_view_state,
                type=Qt.QueuedConnection)

    @property
    def preserve_state_on_switch(self):
        ans = getattr(self, '_preserve_state_on_switch', None)
        if ans is None:
            self._preserve_state_on_switch = ans = \
                self.gui.library_view.preserve_state(require_selected_ids=False)
        return ans

    def pick_random(self, *args):
        self.gui.iactions['Pick Random Book'].pick_random()

    def exim_data(self):
        if isportable:
            return error_dialog(self.gui, _('Cannot export/import'), _(
                'You are running calibre portable, all calibre data is already in the'
                ' calibre portable folder. Export/import is unavailable.'), show=True)
        if self.gui.job_manager.has_jobs():
            return error_dialog(self.gui, _('Cannot export/import'),
                    _('Cannot export/import data while there are running jobs.'), show=True)
        from calibre.gui2.dialogs.exim import EximDialog
        d = EximDialog(parent=self.gui)
        if d.exec_() == d.Accepted:
            if d.restart_needed:
                self.gui.iactions['Restart'].restart()

    def library_name(self):
        db = self.gui.library_view.model().db
        path = db.library_path
        if isbytestring(path):
            path = path.decode(filesystem_encoding)
        path = path.replace(os.sep, '/')
        return self.stats.pretty(path)

    def update_tooltip(self, count):
        tooltip = self.action_spec[2] + '\n\n' + ngettext('{0} [{1} book]', '{0} [{1} books]', count).format(
            getattr(self, 'last_lname', ''), count)
        a = self.qaction
        a.setToolTip(tooltip)
        a.setStatusTip(tooltip)
        a.setWhatsThis(tooltip)

    def library_changed(self, db):
        lname = self.stats.library_used(db)
        self.last_lname = lname
        if len(lname) > 16:
            lname = lname[:16] + u'…'
        a = self.qaction
        a.setText(lname.replace('&', '&&&'))  # I have no idea why this requires a triple ampersand
        self.update_tooltip(db.count())
        self.build_menus()
        state = self.view_state_map.get(self.stats.canonicalize_path(
            db.library_path), None)
        if state is not None:
            self.restore_view_state.emit(state)

    def _restore_view_state(self, state):
        self.preserve_state_on_switch.state = state

    def initialization_complete(self):
        self.library_changed(self.gui.library_view.model().db)

    def build_menus(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            return
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))

        for ac in self.switch_actions:
            ac.setVisible(False)
        self.quick_menu.clear()
        self.rename_menu.clear()
        self.delete_menu.clear()
        quick_actions, rename_actions, delete_actions = [], [], []
        for name, loc in locations:
            name = name.replace('&', '&&')
            ac = self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested,
                loc)))
            ac.setStatusTip(_('Switch to: %s') % loc)
            quick_actions.append(ac)
            ac = self.rename_menu.addAction(name, Dispatcher(partial(self.rename_requested,
                name, loc)))
            rename_actions.append(ac)
            ac.setStatusTip(_('Rename: %s') % loc)
            ac = self.delete_menu.addAction(name, Dispatcher(partial(self.delete_requested,
                name, loc)))
            delete_actions.append(ac)
            ac.setStatusTip(_('Remove: %s') % loc)

        qs_actions = []
        locations_by_frequency = locations
        if len(locations) >= tweaks['many_libraries']:
            locations_by_frequency = list(self.stats.locations(db, limit=sys.maxsize))
        for i, x in enumerate(locations_by_frequency[:len(self.switch_actions)]):
            name, loc = x
            name = name.replace('&', '&&')
            ac = self.switch_actions[i]
            ac.setText(name)
            ac.setStatusTip(_('Switch to: %s') % loc)
            ac.setVisible(True)
            qs_actions.append(ac)
        self.qs_locations = [i[1] for i in locations_by_frequency]

        self.quick_menu_action.setVisible(bool(locations))
        self.rename_menu_action.setVisible(bool(locations))
        self.delete_menu_action.setVisible(bool(locations))
        self.gui.location_manager.set_switch_actions(quick_actions,
                rename_actions, delete_actions, qs_actions,
                self.action_choose)
        # Allow the cloned actions in the OS X global menubar to update
        for a in (self.qaction, self.menuless_qaction):
            a.changed.emit()

    def location_selected(self, loc):
        enabled = loc == 'library'
        self.qaction.setEnabled(enabled)

    def rename_requested(self, name, location):
        LibraryDatabase = db_class()
        loc = location.replace('/', os.sep)
        base = os.path.dirname(loc)
        old_name = name.replace('&&', '&')
        newname, ok = QInputDialog.getText(self.gui, _('Rename') + ' ' + old_name,
                '<p>'+_('Choose a new name for the library <b>%s</b>. ')%name +
                '<p>'+_('Note that the actual library folder will be renamed.'),
                text=old_name)
        newname = sanitize_file_name_unicode(unicode(newname))
        if not ok or not newname or newname == old_name:
            return
        newloc = os.path.join(base, newname)
        if os.path.exists(newloc):
            return error_dialog(self.gui, _('Already exists'),
                    _('The folder %s already exists. Delete it first.') %
                    newloc, show=True)
        if (iswindows and len(newloc) >
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(self.gui, _('Too long'),
                    _('Path to library too long. Must be less than'
                    ' %d characters.')%LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                    show=True)
        if not os.path.exists(loc):
            error_dialog(self.gui, _('Not found'),
                    _('Cannot rename as no library was found at %s. '
                      'Try switching to this library first, then switch back '
                      'and retry the renaming.')%loc, show=True)
            return
        try:
            os.rename(loc, newloc)
        except:
            import traceback
            det_msg = 'Location: %r New Location: %r\n%s'%(loc, newloc,
                                                        traceback.format_exc())
            error_dialog(self.gui, _('Rename failed'),
                    _('Failed to rename the library at %s. '
                'The most common cause for this is if one of the files'
                ' in the library is open in another program.') % loc,
                    det_msg=det_msg, show=True)
            return
        self.stats.rename(location, newloc)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def delete_requested(self, name, location):
        loc = location.replace('/', os.sep)
        if not question_dialog(
                self.gui, _('Library removed'), _(
                'The library %s has been removed from calibre. '
                'The files remain on your computer, if you want '
                'to delete them, you will have to do so manually.') % ('<code>%s</code>' % loc),
                override_icon='dialog_information.png',
                yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'):
            return
        self.stats.remove(location)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()
        if os.path.exists(loc):
            open_local_file(loc)

    def backup_status(self, location):
        self.__backup_status_dialog = d = BackupStatus(self.gui)
        d.show()

    def mark_dirty(self):
        db = self.gui.library_view.model().db
        db.dirtied(list(db.data.iterallids()))
        info_dialog(self.gui, _('Backup metadata'),
            _('Metadata will be backed up while calibre is running, at the '
              'rate of approximately 1 book every three seconds.'), show=True)

    def restore_database(self):
        LibraryDatabase = db_class()
        m = self.gui.library_view.model()
        db = m.db
        if (iswindows and len(db.library_path) >
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(self.gui, _('Too long'),
                    _('Path to library too long. Must be less than'
                    ' %d characters. Move your library to a location with'
                    ' a shorter path using Windows Explorer, then point'
                    ' calibre to the new location and try again.')%
                    LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                    show=True)

        from calibre.gui2.dialogs.restore_library import restore_database
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True
        if restore_database(db, self.gui):
            self.gui.library_moved(db.library_path, call_close=False)

    def check_library(self):
        from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
        self.gui.library_view.save_state()
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True
        library_path = db.library_path

        d = DBCheck(self.gui, db)
        d.start()
        try:
            m.close()
        except:
            pass
        d.break_cycles()
        self.gui.library_moved(library_path, call_close=False)
        if d.rejected:
            return
        if d.error is None:
            if not question_dialog(self.gui, _('Success'),
                    _('Found no errors in your calibre library database.'
                        ' Do you want calibre to check if the files in your '
                        ' library match the information in the database?')):
                return
        else:
            return error_dialog(self.gui, _('Failed'),
                    _('Database integrity check failed, click Show details'
                        ' for details.'), show=True, det_msg=d.error[1])

        self.gui.status_bar.show_message(
                _('Starting library scan, this may take a while'))
        try:
            QCoreApplication.processEvents()
            d = CheckLibraryDialog(self.gui, m.db)

            if not d.do_exec():
                info_dialog(self.gui, _('No problems found'),
                        _('The files in your library match the information '
                        'in the database.'), show=True)
        finally:
            self.gui.status_bar.clear_message()

    def look_for_portable_lib(self, db, location):
        base = get_portable_base()
        if base is None:
            return False, None
        loc = location.replace('/', os.sep)
        candidate = os.path.join(base, os.path.basename(loc))
        if db.exists_at(candidate):
            newloc = candidate.replace(os.sep, '/')
            self.stats.rename(location, newloc)
            return True, newloc
        return False, None

    def switch_requested(self, location):
        if not self.change_library_allowed():
            return
        db = self.gui.library_view.model().db
        current_lib = self.stats.canonicalize_path(db.library_path)
        self.view_state_map[current_lib] = self.preserve_state_on_switch.state
        loc = location.replace('/', os.sep)
        exists = db.exists_at(loc)
        if not exists:
            exists, new_location = self.look_for_portable_lib(db, location)
            if exists:
                location = new_location
                loc = location.replace('/', os.sep)

        if not exists:
            d = MovedDialog(self.stats, location, self.gui)
            ret = d.exec_()
            self.build_menus()
            self.gui.iactions['Copy To Library'].build_menus()
            if ret == d.Accepted:
                loc = d.newloc.replace('/', os.sep)
            else:
                return

        # from calibre.utils.mem import memory
        # import weakref
        # from PyQt5.Qt import QTimer
        # self.dbref = weakref.ref(self.gui.library_view.model().db)
        # self.before_mem = memory()
        self.gui.library_moved(loc, allow_rebuild=True)
        # QTimer.singleShot(5000, self.debug_leak)

    def debug_leak(self):
        import gc
        from calibre.utils.mem import memory
        ref = self.dbref
        for i in xrange(3):
            gc.collect()
        if ref() is not None:
            print 'DB object alive:', ref()
            for r in gc.get_referrers(ref())[:10]:
                print r
                print
        print 'before:', self.before_mem
        print 'after:', memory()
        print
        self.dbref = self.before_mem = None

    def qs_requested(self, idx, *args):
        self.switch_requested(self.qs_locations[idx])

    def count_changed(self, new_count):
        self.update_tooltip(new_count)

    def choose_library(self, *args):
        if not self.change_library_allowed():
            return
        from calibre.gui2.dialogs.choose_library import ChooseLibrary
        self.gui.library_view.save_state()
        db = self.gui.library_view.model().db
        location = self.stats.canonicalize_path(db.library_path)
        self.pre_choose_dialog_location = location
        c = ChooseLibrary(db, self.choose_library_callback, self.gui)
        c.exec_()

    def choose_library_callback(self, newloc, copy_structure=False, library_renamed=False):
        self.gui.library_moved(newloc, copy_structure=copy_structure,
                allow_rebuild=True)
        if library_renamed:
            self.stats.rename(self.pre_choose_dialog_location, prefs['library_path'])
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def change_library_allowed(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            warning_dialog(self.gui, _('Not allowed'),
                    _('You cannot change libraries while using the environment'
                        ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True)
            return False
        if self.gui.job_manager.has_jobs():
            warning_dialog(self.gui, _('Not allowed'),
                    _('You cannot change libraries while jobs'
                        ' are running.'), show=True)
            return False

        if self.gui.proceed_question.questions:
            warning_dialog(self.gui, _('Not allowed'),
                    _('You cannot change libraries until all'
                        ' updates are accepted or rejected.'), show=True)
            return False

        return True
示例#17
0
class Editor(QWidget):  # {{{

    toolbar_prefs_name = None
    data_changed = pyqtSignal()

    def __init__(self,
                 parent=None,
                 one_line_toolbar=False,
                 toolbar_prefs_name=None):
        QWidget.__init__(self, parent)
        self.toolbar_prefs_name = toolbar_prefs_name or self.toolbar_prefs_name
        self.toolbar1 = QToolBar(self)
        self.toolbar2 = QToolBar(self)
        self.toolbar3 = QToolBar(self)
        for i in range(1, 4):
            t = getattr(self, 'toolbar%d' % i)
            t.setIconSize(QSize(18, 18))
        self.editor = EditorWidget(self)
        self.editor.data_changed.connect(self.data_changed)
        self.set_base_url = self.editor.set_base_url
        self.set_html = self.editor.set_html
        self.tabs = QTabWidget(self)
        self.tabs.setTabPosition(self.tabs.South)
        self.wyswyg = QWidget(self.tabs)
        self.code_edit = QPlainTextEdit(self.tabs)
        self.source_dirty = False
        self.wyswyg_dirty = True

        self._layout = QVBoxLayout(self)
        self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
        self.setLayout(self._layout)
        l.setContentsMargins(0, 0, 0, 0)
        if one_line_toolbar:
            tb = QHBoxLayout()
            l.addLayout(tb)
        else:
            tb = l
        tb.addWidget(self.toolbar1)
        tb.addWidget(self.toolbar2)
        tb.addWidget(self.toolbar3)
        l.addWidget(self.editor)
        self._layout.addWidget(self.tabs)
        self.tabs.addTab(self.wyswyg, _('N&ormal view'))
        self.tabs.addTab(self.code_edit, _('&HTML source'))
        self.tabs.currentChanged[int].connect(self.change_tab)
        self.highlighter = Highlighter(self.code_edit.document())
        self.layout().setContentsMargins(0, 0, 0, 0)
        if self.toolbar_prefs_name is not None:
            hidden = gprefs.get(self.toolbar_prefs_name)
            if hidden:
                self.hide_toolbars()

        # toolbar1 {{{
        self.toolbar1.addAction(self.editor.action_undo)
        self.toolbar1.addAction(self.editor.action_redo)
        self.toolbar1.addAction(self.editor.action_select_all)
        self.toolbar1.addAction(self.editor.action_remove_format)
        self.toolbar1.addAction(self.editor.action_clear)
        self.toolbar1.addSeparator()

        for x in ('copy', 'cut', 'paste'):
            ac = getattr(self.editor, 'action_' + x)
            self.toolbar1.addAction(ac)

        self.toolbar1.addSeparator()
        self.toolbar1.addAction(self.editor.action_background)
        # }}}

        # toolbar2 {{{
        for x in ('', 'un'):
            ac = getattr(self.editor, 'action_%sordered_list' % x)
            self.toolbar2.addAction(ac)
        self.toolbar2.addSeparator()
        for x in ('superscript', 'subscript', 'indent', 'outdent'):
            self.toolbar2.addAction(getattr(self.editor, 'action_' + x))
            if x in ('subscript', 'outdent'):
                self.toolbar2.addSeparator()

        self.toolbar2.addAction(self.editor.action_block_style)
        w = self.toolbar2.widgetForAction(self.editor.action_block_style)
        if hasattr(w, 'setPopupMode'):
            w.setPopupMode(w.InstantPopup)
        self.toolbar2.addAction(self.editor.action_insert_link)
        self.toolbar2.addAction(self.editor.action_insert_hr)
        # }}}

        # toolbar3 {{{
        for x in ('bold', 'italic', 'underline', 'strikethrough'):
            ac = getattr(self.editor, 'action_' + x)
            self.toolbar3.addAction(ac)
        self.toolbar3.addSeparator()

        for x in ('left', 'center', 'right', 'justified'):
            ac = getattr(self.editor, 'action_align_' + x)
            self.toolbar3.addAction(ac)
        self.toolbar3.addSeparator()
        self.toolbar3.addAction(self.editor.action_color)
        # }}}

        self.code_edit.textChanged.connect(self.code_dirtied)
        self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)

    def set_minimum_height_for_editor(self, val):
        self.editor.setMinimumHeight(val)

    @property
    def html(self):
        self.tabs.setCurrentIndex(0)
        return self.editor.html

    @html.setter
    def html(self, v):
        self.editor.html = v

    def change_tab(self, index):
        # print 'reloading:', (index and self.wyswyg_dirty) or (not index and
        #        self.source_dirty)
        if index == 1:  # changing to code view
            if self.wyswyg_dirty:
                self.code_edit.setPlainText(self.editor.html)
                self.wyswyg_dirty = False
        elif index == 0:  # changing to wyswyg
            if self.source_dirty:
                self.editor.html = unicode_type(self.code_edit.toPlainText())
                self.source_dirty = False

    @property
    def tab(self):
        return 'code' if self.tabs.currentWidget(
        ) is self.code_edit else 'wyswyg'

    @tab.setter
    def tab(self, val):
        self.tabs.setCurrentWidget(self.code_edit if val ==
                                   'code' else self.wyswyg)

    def wyswyg_dirtied(self, *args):
        self.wyswyg_dirty = True

    def code_dirtied(self, *args):
        self.source_dirty = True

    def hide_toolbars(self):
        self.toolbar1.setVisible(False)
        self.toolbar2.setVisible(False)
        self.toolbar3.setVisible(False)

    def show_toolbars(self):
        self.toolbar1.setVisible(True)
        self.toolbar2.setVisible(True)
        self.toolbar3.setVisible(True)

    def toggle_toolbars(self):
        visible = self.toolbars_visible
        getattr(self, ('hide' if visible else 'show') + '_toolbars')()
        if self.toolbar_prefs_name is not None:
            gprefs.set(self.toolbar_prefs_name, visible)

    @property
    def toolbars_visible(self):
        return self.toolbar1.isVisible() or self.toolbar2.isVisible(
        ) or self.toolbar3.isVisible()

    @toolbars_visible.setter
    def toolbars_visible(self, val):
        getattr(self, ('show' if val else 'hide') + '_toolbars')()

    def set_readonly(self, what):
        self.editor.set_readonly(what)

    def hide_tabs(self):
        self.tabs.tabBar().setVisible(False)
示例#18
0
class AddCover(Dialog):

    import_requested = pyqtSignal(object, object)

    def __init__(self, container, parent=None):
        self.container = container
        Dialog.__init__(self, _('Add a cover'), 'add-cover-wizard', parent)

    @property
    def image_names(self):
        img_types = {guess_type('a.' + x) for x in ('png', 'jpeg', 'gif')}
        for name, mt in self.container.mime_map.iteritems():
            if mt.lower() in img_types:
                yield name

    def setup_ui(self):
        self.l = l = QVBoxLayout(self)
        self.setLayout(l)
        self.gb = gb = QGroupBox(_('&Images in book'), self)
        self.v = v = QVBoxLayout(gb)
        gb.setLayout(v), gb.setFlat(True)
        self.names, self.names_filter = create_filterable_names_list(
            sorted(self.image_names, key=sort_key),
            filter_text=_('Filter the list of images'),
            parent=self)
        self.names.doubleClicked.connect(self.double_clicked,
                                         type=Qt.QueuedConnection)
        self.cover_view = CoverView(self)
        l.addWidget(self.names_filter)
        v.addWidget(self.names)

        self.splitter = s = QSplitter(self)
        l.addWidget(s)
        s.addWidget(gb)
        s.addWidget(self.cover_view)

        self.h = h = QHBoxLayout()
        self.preserve = p = QCheckBox(_('Preserve aspect ratio'))
        p.setToolTip(
            textwrap.fill(
                _('If enabled the cover image you select will be embedded'
                  ' into the book in such a way that when viewed, its aspect'
                  ' ratio (ratio of width to height) will be preserved.'
                  ' This will mean blank spaces around the image if the screen'
                  ' the book is being viewed on has an aspect ratio different'
                  ' to the image.')))
        p.setChecked(tprefs['add_cover_preserve_aspect_ratio'])
        p.setVisible(self.container.book_type != 'azw3')
        p.stateChanged.connect(lambda s: tprefs.set(
            'add_cover_preserve_aspect_ratio', s == Qt.Checked))
        self.info_label = il = QLabel('\xa0')
        h.addWidget(p), h.addStretch(1), h.addWidget(il)
        l.addLayout(h)

        l.addWidget(self.bb)
        b = self.bb.addButton(_('Import &image'), self.bb.ActionRole)
        b.clicked.connect(self.import_image)
        b.setIcon(QIcon(I('document_open.png')))
        self.names.setFocus(Qt.OtherFocusReason)
        self.names.selectionModel().currentChanged.connect(
            self.current_image_changed)
        cname = get_raster_cover_name(self.container)
        if cname:
            row = self.names.model().find_name(cname)
            if row > -1:
                self.names.setCurrentIndex(self.names.model().index(row))

    def double_clicked(self):
        self.accept()

    @property
    def file_name(self):
        return self.names.model().name_for_index(self.names.currentIndex())

    def current_image_changed(self):
        self.info_label.setText('')
        name = self.file_name
        if name is not None:
            data = self.container.raw_data(name, decode=False)
            self.cover_view.set_pixmap(data)
            self.info_label.setText('{0}x{1}px | {2}'.format(
                self.cover_view.pixmap.width(),
                self.cover_view.pixmap.height(), human_readable(len(data))))

    def import_image(self):
        ans = choose_images(self,
                            'add-cover-choose-image',
                            _('Choose a cover image'),
                            formats=('jpg', 'jpeg', 'png', 'gif'))
        if ans:
            from calibre.gui2.tweak_book.file_list import NewFileDialog
            d = NewFileDialog(self)
            d.do_import_file(ans[0], hide_button=True)
            if d.exec_() == d.Accepted:
                self.import_requested.emit(d.file_name, d.file_data)
                self.container = current_container()
                self.names_filter.clear()
                self.names.model().set_names(
                    sorted(self.image_names, key=sort_key))
                i = self.names.model().find_name(d.file_name)
                self.names.setCurrentIndex(self.names.model().index(i))
                self.current_image_changed()

    @classmethod
    def test(cls):
        import sys
        from calibre.ebooks.oeb.polish.container import get_container
        c = get_container(sys.argv[-1], tweak_mode=True)
        d = cls(c)
        if d.exec_() == d.Accepted:
            pass
示例#19
0
class EditorWidget(QWebView, LineEditECM):  # {{{

    data_changed = pyqtSignal()

    def __init__(self, parent=None):
        QWebView.__init__(self, parent)
        self.base_url = None
        self._parent = weakref.ref(parent)
        self.readonly = False

        self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)

        extra_shortcuts = {
            'ToggleBold': 'Bold',
            'ToggleItalic': 'Italic',
            'ToggleUnderline': 'Underline',
        }

        for wac, name, icon, text, checkable in [
            ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True),
            ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'),
             True),
            ('ToggleUnderline', 'underline', 'format-text-underline',
             _('Underline'), True),
            ('ToggleStrikethrough', 'strikethrough',
             'format-text-strikethrough', _('Strikethrough'), True),
            ('ToggleSuperscript', 'superscript', 'format-text-superscript',
             _('Superscript'), True),
            ('ToggleSubscript', 'subscript', 'format-text-subscript',
             _('Subscript'), True),
            ('InsertOrderedList', 'ordered_list', 'format-list-ordered',
             _('Ordered list'), True),
            ('InsertUnorderedList', 'unordered_list', 'format-list-unordered',
             _('Unordered list'), True),
            ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'),
             False),
            ('AlignCenter', 'align_center', 'format-justify-center',
             _('Align center'), False),
            ('AlignRight', 'align_right', 'format-justify-right',
             _('Align right'), False),
            ('AlignJustified', 'align_justified', 'format-justify-fill',
             _('Align justified'), False),
            ('Undo', 'undo', 'edit-undo', _('Undo'), False),
            ('Redo', 'redo', 'edit-redo', _('Redo'), False),
            ('RemoveFormat', 'remove_format', 'edit-clear',
             _('Remove formatting'), False),
            ('Copy', 'copy', 'edit-copy', _('Copy'), False),
            ('Paste', 'paste', 'edit-paste', _('Paste'), False),
            ('Cut', 'cut', 'edit-cut', _('Cut'), False),
            ('Indent', 'indent', 'format-indent-more',
             _('Increase indentation'), False),
            ('Outdent', 'outdent', 'format-indent-less',
             _('Decrease indentation'), False),
            ('SelectAll', 'select_all', 'edit-select-all', _('Select all'),
             False),
        ]:
            ac = PageAction(wac, icon, text, checkable, self)
            setattr(self, 'action_' + name, ac)
            ss = extra_shortcuts.get(wac, None)
            if ss:
                ac.setShortcut(QKeySequence(getattr(QKeySequence, ss)))
            if wac == 'RemoveFormat':
                ac.triggered.connect(self.remove_format_cleanup,
                                     type=Qt.QueuedConnection)

        self.action_color = QAction(QIcon(I('format-text-color.png')),
                                    _('Foreground color'), self)
        self.action_color.triggered.connect(self.foreground_color)

        self.action_background = QAction(QIcon(I('format-fill-color.png')),
                                         _('Background color'), self)
        self.action_background.triggered.connect(self.background_color)

        self.action_block_style = QAction(QIcon(I('format-text-heading.png')),
                                          _('Style text block'), self)
        self.action_block_style.setToolTip(_('Style the selected text block'))
        self.block_style_menu = QMenu(self)
        self.action_block_style.setMenu(self.block_style_menu)
        self.block_style_actions = []
        for text, name in [
            (_('Normal'), 'p'),
            (_('Heading') + ' 1', 'h1'),
            (_('Heading') + ' 2', 'h2'),
            (_('Heading') + ' 3', 'h3'),
            (_('Heading') + ' 4', 'h4'),
            (_('Heading') + ' 5', 'h5'),
            (_('Heading') + ' 6', 'h6'),
            (_('Pre-formatted'), 'pre'),
            (_('Blockquote'), 'blockquote'),
            (_('Address'), 'address'),
        ]:
            ac = BlockStyleAction(text, name, self)
            self.block_style_menu.addAction(ac)
            self.block_style_actions.append(ac)

        self.action_insert_link = QAction(QIcon(I('insert-link.png')),
                                          _('Insert link or image'), self)
        self.action_insert_hr = QAction(QIcon(I('format-text-hr.png')),
                                        _('Insert separator'), self)
        self.action_insert_link.triggered.connect(self.insert_link)
        self.action_insert_hr.triggered.connect(self.insert_hr)
        self.pageAction(QWebPage.ToggleBold).changed.connect(
            self.update_link_action)
        self.action_insert_link.setEnabled(False)
        self.action_insert_hr.setEnabled(False)
        self.action_clear = QAction(QIcon(I('trash.png')), _('Clear'), self)
        self.action_clear.triggered.connect(self.clear_text)

        self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
        self.page().linkClicked.connect(self.link_clicked)
        secure_web_page(self.page().settings())

        self.setHtml('')
        self.set_readonly(False)
        self.page().contentsChanged.connect(self.data_changed)

    def update_link_action(self):
        wac = self.pageAction(QWebPage.ToggleBold).isEnabled()
        self.action_insert_link.setEnabled(wac)
        self.action_insert_hr.setEnabled(wac)

    def set_readonly(self, what):
        self.readonly = what
        self.page().setContentEditable(not self.readonly)

    def clear_text(self, *args):
        us = self.page().undoStack()
        us.beginMacro('clear all text')
        self.action_select_all.trigger()
        self.action_remove_format.trigger()
        self.exec_command('delete')
        us.endMacro()
        self.set_font_style()
        self.setFocus(Qt.OtherFocusReason)

    def link_clicked(self, url):
        open_url(url)

    def foreground_color(self):
        col = QColorDialog.getColor(Qt.black, self,
                                    _('Choose foreground color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('foreColor', unicode_type(col.name()))

    def background_color(self):
        col = QColorDialog.getColor(Qt.white, self,
                                    _('Choose background color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('hiliteColor', unicode_type(col.name()))

    def insert_hr(self, *args):
        self.exec_command('insertHTML', '<hr>')

    def insert_link(self, *args):
        link, name, is_image = self.ask_link()
        if not link:
            return
        url = self.parse_link(link)
        if url.isValid():
            url = unicode_type(url.toString(NO_URL_FORMATTING))
            self.setFocus(Qt.OtherFocusReason)
            if is_image:
                self.exec_command(
                    'insertHTML', '<img src="%s" alt="%s"></img>' %
                    (prepare_string_for_xml(url, True),
                     prepare_string_for_xml(name or _('Image'), True)))
            elif name:
                self.exec_command(
                    'insertHTML',
                    '<a href="%s">%s</a>' % (prepare_string_for_xml(
                        url, True), prepare_string_for_xml(name)))
            else:
                self.exec_command('createLink', url)
        else:
            error_dialog(self,
                         _('Invalid URL'),
                         _('The url %r is invalid') % link,
                         show=True)

    def ask_link(self):
        d = QDialog(self)
        d.setWindowTitle(_('Create link'))
        l = QFormLayout()
        l.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
        d.setLayout(l)
        d.url = QLineEdit(d)
        d.name = QLineEdit(d)
        d.treat_as_image = QCheckBox(d)
        d.setMinimumWidth(600)
        d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        d.br = b = QPushButton(_('&Browse'))
        b.setIcon(QIcon(I('document_open.png')))

        def cf():
            files = choose_files(d,
                                 'select link file',
                                 _('Choose file'),
                                 select_only_single_file=True)
            if files:
                path = files[0]
                d.url.setText(path)
                if path and os.path.exists(path):
                    with lopen(path, 'rb') as f:
                        q = what(f)
                    is_image = q in {'jpeg', 'png', 'gif'}
                    d.treat_as_image.setChecked(is_image)

        b.clicked.connect(cf)
        d.la = la = QLabel(
            _('Enter a URL. If you check the "Treat the URL as an image" box '
              'then the URL will be added as an image reference instead of as '
              'a link. You can also choose to create a link to a file on '
              'your computer. '
              'Note that if you create a link to a file on your computer, it '
              'will stop working if the file is moved.'))
        la.setWordWrap(True)
        la.setStyleSheet('QLabel { margin-bottom: 1.5ex }')
        l.setWidget(0, l.SpanningRole, la)
        l.addRow(_('Enter &URL:'), d.url)
        l.addRow(_('Treat the URL as an &image'), d.treat_as_image)
        l.addRow(_('Enter &name (optional):'), d.name)
        l.addRow(_('Choose a file on your computer:'), d.br)
        l.addRow(d.bb)
        d.bb.accepted.connect(d.accept)
        d.bb.rejected.connect(d.reject)
        d.resize(d.sizeHint())
        link, name, is_image = None, None, False
        if d.exec_() == d.Accepted:
            link, name = unicode_type(d.url.text()).strip(), unicode_type(
                d.name.text()).strip()
            is_image = d.treat_as_image.isChecked()
        return link, name, is_image

    def parse_link(self, link):
        link = link.strip()
        if link and os.path.exists(link):
            return QUrl.fromLocalFile(link)
        has_schema = re.match(r'^[a-zA-Z]+:', link)
        if has_schema is not None:
            url = QUrl(link, QUrl.TolerantMode)
            if url.isValid():
                return url
        if os.path.exists(link):
            return QUrl.fromLocalFile(link)

        if has_schema is None:
            first, _, rest = link.partition('.')
            prefix = 'http'
            if first == 'ftp':
                prefix = 'ftp'
            url = QUrl(prefix + '://' + link, QUrl.TolerantMode)
            if url.isValid():
                return url

        return QUrl(link, QUrl.TolerantMode)

    def sizeHint(self):
        return QSize(150, 150)

    def exec_command(self, cmd, arg=None):
        frame = self.page().mainFrame()
        if arg is not None:
            js = 'document.execCommand("%s", false, %s);' % (
                cmd, json.dumps(unicode_type(arg)))
        else:
            js = 'document.execCommand("%s", false, null);' % cmd
        frame.evaluateJavaScript(js)

    def remove_format_cleanup(self):
        self.html = self.html

    @property
    def html(self):
        ans = ''
        try:
            if not self.page().mainFrame().documentElement().findFirst(
                    'meta[name="calibre-dont-sanitize"]').isNull():
                # Bypass cleanup if special meta tag exists
                return unicode_type(self.page().mainFrame().toHtml())
            check = unicode_type(self.page().mainFrame().toPlainText()).strip()
            raw = unicode_type(self.page().mainFrame().toHtml())
            raw = xml_to_unicode(raw,
                                 strip_encoding_pats=True,
                                 resolve_entities=True)[0]
            raw = self.comments_pat.sub('', raw)
            if not check and '<img' not in raw.lower():
                return ans

            try:
                root = html.fromstring(raw)
            except Exception:
                root = parse(raw, maybe_xhtml=False, sanitize_names=True)

            elems = []
            for body in root.xpath('//body'):
                if body.text:
                    elems.append(body.text)
                elems += [
                    html.tostring(x, encoding='unicode') for x in body
                    if x.tag not in ('script', 'style')
                ]

            if len(elems) > 1:
                ans = '<div>%s</div>' % (''.join(elems))
            else:
                ans = ''.join(elems)
                if not ans.startswith('<'):
                    ans = '<p>%s</p>' % ans
            ans = xml_replace_entities(ans)
        except:
            import traceback
            traceback.print_exc()

        return ans

    @html.setter
    def html(self, val):
        if self.base_url is None:
            self.setHtml(val)
        else:
            self.setHtml(val, self.base_url)
        self.set_font_style()

    def set_base_url(self, qurl):
        self.base_url = qurl
        self.setHtml('', self.base_url)

    def set_html(self, val, allow_undo=True):
        if not allow_undo or self.readonly:
            self.html = val
            return
        mf = self.page().mainFrame()
        mf.evaluateJavaScript('document.execCommand("selectAll", false, null)')
        mf.evaluateJavaScript('document.execCommand("insertHTML", false, %s)' %
                              json.dumps(unicode_type(val)))
        self.set_font_style()

    def set_font_style(self):
        fi = QFontInfo(QApplication.font(self))
        f = fi.pixelSize() + 1 + int(
            tweaks['change_book_details_font_size_by'])
        fam = unicode_type(fi.family()).strip().replace('"', '')
        if not fam:
            fam = 'sans-serif'
        style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam)

        # toList() is needed because PyQt on Debian is old/broken
        for body in self.page().mainFrame().documentElement().findAll(
                'body').toList():
            body.setAttribute('style', style)
        self.page().setContentEditable(not self.readonly)

    def event(self, ev):
        if ev.type() in (ev.KeyPress,
                         ev.KeyRelease, ev.ShortcutOverride) and hasattr(
                             ev,
                             'key') and ev.key() in (Qt.Key_Tab, Qt.Key_Escape,
                                                     Qt.Key_Backtab):
            if (ev.key() == Qt.Key_Tab and ev.modifiers() & Qt.ControlModifier
                    and ev.type() == ev.KeyPress):
                self.exec_command('insertHTML',
                                  '<span style="white-space:pre">\t</span>')
                ev.accept()
                return True
            ev.ignore()
            return False
        return QWebView.event(self, ev)

    def text(self):
        return self.page().selectedText()

    def setText(self, text):
        self.exec_command('insertText', text)

    def contextMenuEvent(self, ev):
        menu = self.page().createStandardContextMenu()
        paste = self.pageAction(QWebPage.Paste)
        for action in menu.actions():
            if action == paste:
                menu.insertAction(action,
                                  self.pageAction(QWebPage.PasteAndMatchStyle))
        st = self.text()
        if st and st.strip():
            self.create_change_case_menu(menu)
        parent = self._parent()
        if hasattr(parent, 'toolbars_visible'):
            vis = parent.toolbars_visible
            menu.addAction(
                _('%s toolbars') % (_('Hide') if vis else _('Show')),
                parent.toggle_toolbars)
        menu.exec_(ev.globalPos())
示例#20
0
class EbookViewer(MainWindow):

    msg_from_anotherinstance = pyqtSignal(object)
    book_preparation_started = pyqtSignal()
    book_prepared = pyqtSignal(object, object)
    MAIN_WINDOW_STATE_VERSION = 1

    def __init__(self,
                 open_at=None,
                 continue_reading=None,
                 force_reload=False):
        MainWindow.__init__(self, None)
        self.shutting_down = self.close_forced = False
        self.force_reload = force_reload
        connect_lambda(self.book_preparation_started,
                       self,
                       lambda self: self.loading_overlay(
                           _('Preparing book for first read, please wait')),
                       type=Qt.QueuedConnection)
        self.maximized_at_last_fullscreen = False
        self.save_pos_timer = t = QTimer(self)
        t.setSingleShot(True), t.setInterval(3000), t.setTimerType(
            Qt.VeryCoarseTimer)
        connect_lambda(t.timeout, self,
                       lambda self: self.save_annotations(in_book_file=False))
        self.pending_open_at = open_at
        self.base_window_title = _('E-book viewer')
        self.setWindowTitle(self.base_window_title)
        self.in_full_screen_mode = None
        self.image_popup = ImagePopup(self)
        self.actions_toolbar = at = ActionsToolBar(self)
        at.open_book_at_path.connect(self.ask_for_open)
        self.addToolBar(Qt.LeftToolBarArea, at)
        try:
            os.makedirs(annotations_dir)
        except EnvironmentError:
            pass
        self.current_book_data = {}
        self.book_prepared.connect(self.load_finished,
                                   type=Qt.QueuedConnection)
        self.dock_defs = dock_defs()

        def create_dock(title,
                        name,
                        area,
                        areas=Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea):
            ans = QDockWidget(title, self)
            ans.setObjectName(name)
            self.addDockWidget(area, ans)
            ans.setVisible(False)
            ans.visibilityChanged.connect(self.dock_visibility_changed)
            return ans

        for dock_def in itervalues(self.dock_defs):
            setattr(
                self, '{}_dock'.format(dock_def.name.partition('-')[0]),
                create_dock(dock_def.title, dock_def.name,
                            dock_def.initial_area, dock_def.allowed_areas))

        self.toc_container = w = QWidget(self)
        w.l = QVBoxLayout(w)
        self.toc = TOCView(w)
        self.toc.clicked[QModelIndex].connect(self.toc_clicked)
        self.toc.searched.connect(self.toc_searched)
        self.toc_search = TOCSearch(self.toc, parent=w)
        w.l.addWidget(self.toc), w.l.addWidget(
            self.toc_search), w.l.setContentsMargins(0, 0, 0, 0)
        self.toc_dock.setWidget(w)

        self.search_widget = w = SearchPanel(self)
        w.search_requested.connect(self.start_search)
        self.search_dock.setWidget(w)
        self.search_dock.visibilityChanged.connect(
            self.search_widget.visibility_changed)

        self.lookup_widget = w = Lookup(self)
        self.lookup_dock.visibilityChanged.connect(
            self.lookup_widget.visibility_changed)
        self.lookup_dock.setWidget(w)

        self.bookmarks_widget = w = BookmarkManager(self)
        connect_lambda(
            w.create_requested, self, lambda self: self.web_view.
            get_current_cfi(self.bookmarks_widget.create_new_bookmark))
        w.edited.connect(self.bookmarks_edited)
        w.activated.connect(self.bookmark_activated)
        w.toggle_requested.connect(self.toggle_bookmarks)
        self.bookmarks_dock.setWidget(w)

        self.highlights_widget = w = HighlightsPanel(self)
        self.highlights_dock.setWidget(w)

        self.web_view = WebView(self)
        self.web_view.cfi_changed.connect(self.cfi_changed)
        self.web_view.reload_book.connect(self.reload_book)
        self.web_view.toggle_toc.connect(self.toggle_toc)
        self.web_view.show_search.connect(self.show_search)
        self.web_view.find_next.connect(self.search_widget.find_next_requested)
        self.search_widget.show_search_result.connect(
            self.web_view.show_search_result)
        self.web_view.search_result_not_found.connect(
            self.search_widget.search_result_not_found)
        self.web_view.toggle_bookmarks.connect(self.toggle_bookmarks)
        self.web_view.toggle_highlights.connect(self.toggle_highlights)
        self.web_view.new_bookmark.connect(
            self.bookmarks_widget.create_requested)
        self.web_view.toggle_inspector.connect(self.toggle_inspector)
        self.web_view.toggle_lookup.connect(self.toggle_lookup)
        self.web_view.quit.connect(self.quit)
        self.web_view.update_current_toc_nodes.connect(
            self.toc.update_current_toc_nodes)
        self.web_view.toggle_full_screen.connect(self.toggle_full_screen)
        self.web_view.ask_for_open.connect(self.ask_for_open,
                                           type=Qt.QueuedConnection)
        self.web_view.selection_changed.connect(
            self.lookup_widget.selected_text_changed, type=Qt.QueuedConnection)
        self.web_view.view_image.connect(self.view_image,
                                         type=Qt.QueuedConnection)
        self.web_view.copy_image.connect(self.copy_image,
                                         type=Qt.QueuedConnection)
        self.web_view.show_loading_message.connect(self.show_loading_message)
        self.web_view.show_error.connect(self.show_error)
        self.web_view.print_book.connect(self.print_book,
                                         type=Qt.QueuedConnection)
        self.web_view.reset_interface.connect(self.reset_interface,
                                              type=Qt.QueuedConnection)
        self.web_view.quit.connect(self.quit, type=Qt.QueuedConnection)
        self.web_view.shortcuts_changed.connect(self.shortcuts_changed)
        self.web_view.scrollbar_context_menu.connect(
            self.scrollbar_context_menu)
        self.web_view.close_prep_finished.connect(self.close_prep_finished)
        self.web_view.highlights_changed.connect(self.highlights_changed)
        self.actions_toolbar.initialize(self.web_view,
                                        self.search_dock.toggleViewAction())
        self.setCentralWidget(self.web_view)
        self.loading_overlay = LoadingOverlay(self)
        self.restore_state()
        self.actions_toolbar.update_visibility()
        self.dock_visibility_changed()
        if continue_reading:
            self.continue_reading()

    def shortcuts_changed(self, smap):
        rmap = defaultdict(list)
        for k, v in iteritems(smap):
            rmap[v].append(k)
        self.actions_toolbar.set_tooltips(rmap)

    def resizeEvent(self, ev):
        self.loading_overlay.resize(self.size())
        return MainWindow.resizeEvent(self, ev)

    def scrollbar_context_menu(self, x, y, frac):
        m = QMenu(self)
        amap = {}

        def a(text, name):
            m.addAction(text)
            amap[text] = name

        a(_('Scroll here'), 'here')
        m.addSeparator()
        a(_('Start of book'), 'start_of_book')
        a(_('End of book'), 'end_of_book')
        m.addSeparator()
        a(_('Previous section'), 'previous_section')
        a(_('Next section'), 'next_section')
        m.addSeparator()
        a(_('Start of current file'), 'start_of_file')
        a(_('End of current file'), 'end_of_file')
        m.addSeparator()
        a(_('Hide this scrollbar'), 'toggle_scrollbar')

        q = m.exec_(QCursor.pos())
        if not q:
            return
        q = amap[q.text()]
        if q == 'here':
            self.web_view.goto_frac(frac)
        else:
            self.web_view.trigger_shortcut(q)

    # IPC {{{
    def handle_commandline_arg(self, arg):
        if arg:
            if os.path.isfile(arg) and os.access(arg, os.R_OK):
                self.load_ebook(arg)
            else:
                prints('Cannot read from:', arg, file=sys.stderr)

    def another_instance_wants_to_talk(self, msg):
        try:
            path, open_at = msg
        except Exception:
            return
        self.load_ebook(path, open_at=open_at)
        self.raise_()

    # }}}

    # Fullscreen {{{
    def set_full_screen(self, on):
        if on:
            self.maximized_at_last_fullscreen = self.isMaximized()
            if not self.actions_toolbar.visible_in_fullscreen:
                self.actions_toolbar.setVisible(False)
            self.showFullScreen()
        else:
            self.actions_toolbar.update_visibility()
            if self.maximized_at_last_fullscreen:
                self.showMaximized()
            else:
                self.showNormal()

    def changeEvent(self, ev):
        if ev.type() == QEvent.WindowStateChange:
            in_full_screen_mode = self.isFullScreen()
            if self.in_full_screen_mode is None or self.in_full_screen_mode != in_full_screen_mode:
                self.in_full_screen_mode = in_full_screen_mode
                self.web_view.notify_full_screen_state_change(
                    self.in_full_screen_mode)
        return MainWindow.changeEvent(self, ev)

    def toggle_full_screen(self):
        self.set_full_screen(not self.isFullScreen())

    # }}}

    # Docks (ToC, Bookmarks, Lookup, etc.) {{{

    def toggle_inspector(self):
        visible = self.inspector_dock.toggleViewAction().isChecked()
        self.inspector_dock.setVisible(not visible)

    def toggle_toc(self):
        self.toc_dock.setVisible(not self.toc_dock.isVisible())

    def show_search(self):
        self.search_dock.setVisible(True)
        self.search_dock.activateWindow()
        self.search_dock.raise_()
        self.search_widget.focus_input()

    def start_search(self, search_query):
        name = self.web_view.current_content_file
        if name:
            self.search_widget.start_search(search_query, name)
            self.web_view.setFocus(Qt.OtherFocusReason)

    def toggle_bookmarks(self):
        is_visible = self.bookmarks_dock.isVisible()
        self.bookmarks_dock.setVisible(not is_visible)
        if is_visible:
            self.web_view.setFocus(Qt.OtherFocusReason)
        else:
            self.bookmarks_widget.bookmarks_list.setFocus(Qt.OtherFocusReason)

    def toggle_highlights(self):
        is_visible = self.highlights_dock.isVisible()
        self.highlights_dock.setVisible(not is_visible)
        if is_visible:
            self.web_view.setFocus(Qt.OtherFocusReason)
        else:
            self.highlights_widget.focus()

    def toggle_lookup(self):
        self.lookup_dock.setVisible(not self.lookup_dock.isVisible())

    def toc_clicked(self, index):
        item = self.toc_model.itemFromIndex(index)
        self.web_view.goto_toc_node(item.node_id)

    def toc_searched(self, index):
        item = self.toc_model.itemFromIndex(index)
        self.web_view.goto_toc_node(item.node_id)

    def bookmarks_edited(self, bookmarks):
        self.current_book_data['annotations_map']['bookmark'] = bookmarks
        # annotations will be saved in book file on exit
        self.save_annotations(in_book_file=False)

    def bookmark_activated(self, cfi):
        self.web_view.goto_cfi(cfi)

    def view_image(self, name):
        path = get_path_for_name(name)
        if path:
            pmap = QPixmap()
            if pmap.load(path):
                self.image_popup.current_img = pmap
                self.image_popup.current_url = QUrl.fromLocalFile(path)
                self.image_popup()
            else:
                error_dialog(self,
                             _('Invalid image'),
                             _("Failed to load the image {}").format(name),
                             show=True)
        else:
            error_dialog(self,
                         _('Image not found'),
                         _("Failed to find the image {}").format(name),
                         show=True)

    def copy_image(self, name):
        path = get_path_for_name(name)
        if not path:
            return error_dialog(self,
                                _('Image not found'),
                                _("Failed to find the image {}").format(name),
                                show=True)
        try:
            img = image_from_path(path)
        except Exception:
            return error_dialog(self,
                                _('Invalid image'),
                                _("Failed to load the image {}").format(name),
                                show=True)
        url = QUrl.fromLocalFile(path)
        md = QMimeData()
        md.setImageData(img)
        md.setUrls([url])
        QApplication.instance().clipboard().setMimeData(md)

    def dock_visibility_changed(self):
        vmap = {
            dock.objectName().partition('-')[0]:
            dock.toggleViewAction().isChecked()
            for dock in self.dock_widgets
        }
        self.actions_toolbar.update_dock_actions(vmap)

    # }}}

    # Load book {{{

    def show_loading_message(self, msg):
        if msg:
            self.loading_overlay(msg)
        else:
            self.loading_overlay.hide()

    def show_error(self, title, msg, details):
        self.loading_overlay.hide()
        error_dialog(self, title, msg, det_msg=details or None, show=True)

    def print_book(self):
        from .printing import print_book
        print_book(set_book_path.pathtoebook,
                   book_title=self.current_book_data['metadata']['title'],
                   parent=self)

    @property
    def dock_widgets(self):
        return self.findChildren(QDockWidget,
                                 options=Qt.FindDirectChildrenOnly)

    def reset_interface(self):
        for dock in self.dock_widgets:
            dock.setFloating(False)
            area = self.dock_defs[dock.objectName().partition('-')
                                  [0]].initial_area
            self.removeDockWidget(dock)
            self.addDockWidget(area, dock)
            dock.setVisible(False)

        for toolbar in self.findChildren(QToolBar):
            toolbar.setVisible(False)
            self.removeToolBar(toolbar)
            self.addToolBar(Qt.LeftToolBarArea, toolbar)

    def ask_for_open(self, path=None):
        if path is None:
            files = choose_files(self,
                                 'ebook viewer open dialog',
                                 _('Choose e-book'),
                                 [(_('E-books'), available_input_formats())],
                                 all_files=False,
                                 select_only_single_file=True)
            if not files:
                return
            path = files[0]
        self.load_ebook(path)

    def continue_reading(self):
        rl = vprefs['session_data'].get('standalone_recently_opened')
        if rl:
            entry = rl[0]
            self.load_ebook(entry['pathtoebook'])

    def load_ebook(self, pathtoebook, open_at=None, reload_book=False):
        self.web_view.show_home_page_on_ready = False
        if open_at:
            self.pending_open_at = open_at
        self.setWindowTitle(
            _('Loading book') + '… — {}'.format(self.base_window_title))
        self.loading_overlay(_('Loading book, please wait'))
        self.save_annotations()
        self.current_book_data = {}
        self.search_widget.clear_searches()
        t = Thread(name='LoadBook',
                   target=self._load_ebook_worker,
                   args=(pathtoebook, open_at, reload_book
                         or self.force_reload))
        t.daemon = True
        t.start()

    def reload_book(self):
        if self.current_book_data:
            self.load_ebook(self.current_book_data['pathtoebook'],
                            reload_book=True)

    def _load_ebook_worker(self, pathtoebook, open_at, reload_book):
        if DEBUG:
            start_time = monotonic()
        try:
            ans = prepare_book(pathtoebook,
                               force=reload_book,
                               prepare_notify=self.prepare_notify)
        except WorkerError as e:
            self.book_prepared.emit(False, {
                'exception': e,
                'tb': e.orig_tb,
                'pathtoebook': pathtoebook
            })
        except Exception as e:
            import traceback
            self.book_prepared.emit(
                False, {
                    'exception': e,
                    'tb': traceback.format_exc(),
                    'pathtoebook': pathtoebook
                })
        else:
            if DEBUG:
                print('Book prepared in {:.2f} seconds'.format(monotonic() -
                                                               start_time))
            self.book_prepared.emit(
                True, {
                    'base': ans,
                    'pathtoebook': pathtoebook,
                    'open_at': open_at,
                    'reloaded': reload_book
                })

    def prepare_notify(self):
        self.book_preparation_started.emit()

    def load_finished(self, ok, data):
        if self.shutting_down:
            return
        open_at, self.pending_open_at = self.pending_open_at, None
        self.web_view.clear_caches()
        if not ok:
            self.setWindowTitle(self.base_window_title)
            tb = data['tb'].strip()
            tb = re.split(
                r'^calibre\.gui2\.viewer\.convert_book\.ConversionFailure:\s*',
                tb,
                maxsplit=1,
                flags=re.M)[-1]
            last_line = tuple(tb.strip().splitlines())[-1]
            if last_line.startswith('calibre.ebooks.DRMError'):
                DRMErrorMessage(self).exec_()
            else:
                error_dialog(
                    self,
                    _('Loading book failed'),
                    _('Failed to open the book at {0}. Click "Show details" for more info.'
                      ).format(data['pathtoebook']),
                    det_msg=tb,
                    show=True)
            self.loading_overlay.hide()
            self.web_view.show_home_page()
            return
        try:
            set_book_path(data['base'], data['pathtoebook'])
        except Exception:
            if data['reloaded']:
                raise
            self.load_ebook(data['pathtoebook'],
                            open_at=data['open_at'],
                            reload_book=True)
            return
        self.current_book_data = data
        self.current_book_data['annotations_map'] = defaultdict(list)
        self.current_book_data['annotations_path_key'] = path_key(
            data['pathtoebook']) + '.json'
        self.load_book_data()
        self.update_window_title()
        initial_cfi = self.initial_cfi_for_current_book()
        initial_position = {
            'type': 'cfi',
            'data': initial_cfi
        } if initial_cfi else None
        if open_at:
            if open_at.startswith('toc:'):
                initial_toc_node = self.toc_model.node_id_for_text(
                    open_at[len('toc:'):])
                initial_position = {'type': 'toc', 'data': initial_toc_node}
            elif open_at.startswith('toc-href:'):
                initial_toc_node = self.toc_model.node_id_for_href(
                    open_at[len('toc-href:'):], exact=True)
                initial_position = {'type': 'toc', 'data': initial_toc_node}
            elif open_at.startswith('toc-href-contains:'):
                initial_toc_node = self.toc_model.node_id_for_href(
                    open_at[len('toc-href-contains:'):], exact=False)
                initial_position = {'type': 'toc', 'data': initial_toc_node}
            elif open_at.startswith('epubcfi(/'):
                initial_position = {'type': 'cfi', 'data': open_at}
            elif open_at.startswith('ref:'):
                initial_position = {
                    'type': 'ref',
                    'data': open_at[len('ref:'):]
                }
            elif is_float(open_at):
                initial_position = {'type': 'bookpos', 'data': float(open_at)}
        highlights = self.current_book_data['annotations_map']['highlight']
        self.highlights_widget.load(highlights)
        self.web_view.start_book_load(initial_position=initial_position,
                                      highlights=list(
                                          map(serialize_annotation,
                                              highlights)))

    def load_book_data(self):
        self.load_book_annotations()
        path = os.path.join(self.current_book_data['base'],
                            'calibre-book-manifest.json')
        with open(path, 'rb') as f:
            raw = f.read()
        self.current_book_data['manifest'] = manifest = json.loads(raw)
        toc = manifest.get('toc')
        self.toc_model = TOC(toc)
        self.toc.setModel(self.toc_model)
        self.bookmarks_widget.set_bookmarks(
            self.current_book_data['annotations_map']['bookmark'])
        self.current_book_data['metadata'] = set_book_path.parsed_metadata
        self.current_book_data['manifest'] = set_book_path.parsed_manifest

    def load_book_annotations(self):
        amap = self.current_book_data['annotations_map']
        path = os.path.join(self.current_book_data['base'],
                            'calibre-book-annotations.json')
        if os.path.exists(path):
            with open(path, 'rb') as f:
                raw = f.read()
            merge_annotations(json_loads(raw), amap)
        path = os.path.join(annotations_dir,
                            self.current_book_data['annotations_path_key'])
        if os.path.exists(path):
            with open(path, 'rb') as f:
                raw = f.read()
            merge_annotations(parse_annotations(raw), amap)

    def update_window_title(self):
        try:
            title = self.current_book_data['metadata']['title']
        except Exception:
            title = _('Unknown')
        book_format = self.current_book_data['manifest']['book_format']
        title = '{} [{}] — {}'.format(title, book_format,
                                      self.base_window_title)
        self.setWindowTitle(title)

    # }}}

    # CFI management {{{
    def initial_cfi_for_current_book(self):
        lrp = self.current_book_data['annotations_map']['last-read']
        if lrp and get_session_pref('remember_last_read', default=True):
            lrp = lrp[0]
            if lrp['pos_type'] == 'epubcfi':
                return lrp['pos']

    def cfi_changed(self, cfi):
        if not self.current_book_data:
            return
        self.current_book_data['annotations_map']['last-read'] = [{
            'pos':
            cfi,
            'pos_type':
            'epubcfi',
            'timestamp':
            utcnow()
        }]
        self.save_pos_timer.start()

    # }}}

    # State serialization {{{
    def save_annotations(self, in_book_file=True):
        if not self.current_book_data:
            return
        amap = self.current_book_data['annotations_map']
        annots = as_bytes(serialize_annotations(amap))
        with open(
                os.path.join(annotations_dir,
                             self.current_book_data['annotations_path_key']),
                'wb') as f:
            f.write(annots)
        if in_book_file and self.current_book_data.get(
                'pathtoebook',
                '').lower().endswith('.epub') and get_session_pref(
                    'save_annotations_in_ebook', default=True):
            path = self.current_book_data['pathtoebook']
            if os.access(path, os.W_OK):
                before_stat = os.stat(path)
                save_annots_to_epub(path, annots)
                update_book(path, before_stat,
                            {'calibre-book-annotations.json': annots})

    def highlights_changed(self, highlights):
        if not self.current_book_data:
            return
        for h in highlights:
            h['timestamp'] = parse_iso8601(h['timestamp'], assume_utc=True)
        amap = self.current_book_data['annotations_map']
        amap['highlight'] = highlights
        self.save_annotations()

    def save_state(self):
        with vprefs:
            vprefs['main_window_state'] = bytearray(
                self.saveState(self.MAIN_WINDOW_STATE_VERSION))
            vprefs['main_window_geometry'] = bytearray(self.saveGeometry())

    def restore_state(self):
        state = vprefs['main_window_state']
        geom = vprefs['main_window_geometry']
        if geom and get_session_pref('remember_window_geometry',
                                     default=False):
            QApplication.instance().safe_restore_geometry(self, geom)
        else:
            QApplication.instance().ensure_window_on_screen(self)
        if state:
            self.restoreState(state, self.MAIN_WINDOW_STATE_VERSION)
            self.inspector_dock.setVisible(False)

    def quit(self):
        self.close()

    def force_close(self):
        if not self.close_forced:
            self.close_forced = True
            self.quit()

    def close_prep_finished(self, cfi):
        if cfi:
            self.cfi_changed(cfi)
        self.force_close()

    def closeEvent(self, ev):
        if self.current_book_data and self.web_view.view_is_ready and not self.close_forced:
            ev.ignore()
            if not self.shutting_down:
                self.shutting_down = True
                QTimer.singleShot(2000, self.force_close)
                self.web_view.prepare_for_close()
            return
        self.shutting_down = True
        self.search_widget.shutdown()
        try:
            self.save_annotations()
            self.save_state()
        except Exception:
            import traceback
            traceback.print_exc()
        clean_running_workers()
        return MainWindow.closeEvent(self, ev)
示例#21
0
class RecipeList(QWidget):  # {{{

    edit_recipe = pyqtSignal(object, object)

    def __init__(self, parent, model):
        QWidget.__init__(self, parent)

        self.l = l = QHBoxLayout(self)

        self.view = v = QListView(self)
        v.doubleClicked.connect(self.item_activated)
        v.setModel(CustomRecipeModel(model))
        l.addWidget(v)

        self.stacks = s = QStackedWidget(self)
        l.addWidget(s, stretch=10, alignment=Qt.AlignmentFlag.AlignTop)

        self.first_msg = la = QLabel(
            _('Create a new news source by clicking one of the buttons below'))
        la.setWordWrap(True)
        s.addWidget(la)

        self.w = w = QWidget(self)
        w.l = l = QVBoxLayout(w)
        l.setContentsMargins(0, 0, 0, 0)
        s.addWidget(w)

        self.title = la = QLabel(w)
        la.setAlignment(Qt.AlignmentFlag.AlignHCenter
                        | Qt.AlignmentFlag.AlignTop)
        l.addWidget(la)
        l.setSpacing(20)

        self.edit_button = b = QPushButton(QIcon(I('modified.png')),
                                           _('&Edit this recipe'), w)
        b.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
        b.clicked.connect(self.edit_requested)
        l.addWidget(b)
        self.remove_button = b = QPushButton(QIcon(I('list_remove.png')),
                                             _('&Remove this recipe'), w)
        b.clicked.connect(self.remove)
        b.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
        l.addWidget(b)
        self.export_button = b = QPushButton(QIcon(I('save.png')),
                                             _('S&ave recipe as file'), w)
        b.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
        b.clicked.connect(self.save_recipe)
        l.addWidget(b)
        self.download_button = b = QPushButton(
            QIcon(I('download-metadata.png')), _('&Download this recipe'), w)
        b.clicked.connect(self.download)
        b.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
        l.addWidget(b)

        self.select_row()
        v.selectionModel().currentRowChanged.connect(self.recipe_selected)

    def select_row(self, row=0):
        v = self.view
        if v.model().rowCount() > 0:
            idx = v.model().index(row)
            if idx.isValid():
                v.selectionModel().select(idx,
                                          v.selectionModel().ClearAndSelect)
                v.setCurrentIndex(idx)
                self.recipe_selected(idx)

    def add(self, title, src):
        row = self.model.add(title, src)
        self.select_row(row)

    def update(self, row, title, src):
        self.model.update(row, title, src)
        self.select_row(row)

    @property
    def model(self):
        return self.view.model()

    def recipe_selected(self, cur, prev=None):
        if cur.isValid():
            self.stacks.setCurrentIndex(1)
            self.title.setText('<h2 style="text-align:center">%s</h2>' %
                               self.model.title(cur))
        else:
            self.stacks.setCurrentIndex(0)

    def edit_requested(self):
        idx = self.view.currentIndex()
        if idx.isValid():
            src = self.model.script(idx)
            if src is not None:
                self.edit_recipe.emit(idx.row(), src)

    def save_recipe(self):
        idx = self.view.currentIndex()
        if idx.isValid():
            src = self.model.script(idx)
            if src is not None:
                path = choose_save_file(self,
                                        'save-custom-recipe',
                                        _('Save recipe'),
                                        filters=[(_('Recipes'), ['recipe'])],
                                        all_files=False,
                                        initial_filename='{}.recipe'.format(
                                            self.model.title(idx)))
                if path:
                    with open(path, 'wb') as f:
                        f.write(as_bytes(src))

    def item_activated(self, idx):
        if idx.isValid():
            src = self.model.script(idx)
            if src is not None:
                self.edit_recipe.emit(idx.row(), src)

    def remove(self):
        idx = self.view.currentIndex()
        if idx.isValid():
            self.model.remove((idx.row(), ))
            self.select_row()
            if self.model.rowCount() == 0:
                self.stacks.setCurrentIndex(0)

    def download(self):
        idx = self.view.currentIndex()
        if idx.isValid():
            urn = self.model.urn(idx)
            title = self.model.title(idx)
            from calibre.gui2.ui import get_gui
            gui = get_gui()
            gui.iactions['Fetch News'].download_custom_recipe(title, urn)

    def has_title(self, title):
        return self.model.has_title(title)

    def add_many(self, script_map):
        self.model.add_many(script_map)
        self.select_row()

    def replace_many_by_title(self, script_map):
        self.model.replace_many_by_title(script_map)
        self.select_row()
示例#22
0
class ActionsToolBar(ToolBar):

    action_triggered = pyqtSignal(object)
    open_book_at_path = pyqtSignal(object)

    def __init__(self, parent=None):
        ToolBar.__init__(self, parent)
        self.setObjectName('actions_toolbar')
        self.customContextMenuRequested.connect(self.show_context_menu)

    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction(_('Customize this toolbar'))
        a.triggered.connect(self.customize)
        a = m.addAction(_('Hide this toolbar'))
        a.triggered.connect(self.hide_toolbar)
        m.exec_(pos)

    def hide_toolbar(self):
        self.web_view.trigger_shortcut('toggle_toolbar')

    def initialize(self, web_view, toggle_search_action):
        self.web_view = web_view
        shortcut_action = self.create_shortcut_action
        aa = all_actions()
        self.action_triggered.connect(web_view.trigger_shortcut)
        page = web_view.page()
        web_view.paged_mode_changed.connect(self.update_mode_action)
        web_view.reference_mode_changed.connect(
            self.update_reference_mode_action)
        web_view.standalone_misc_settings_changed.connect(
            self.update_visibility)
        web_view.autoscroll_state_changed.connect(
            self.update_autoscroll_action)
        web_view.customize_toolbar.connect(self.customize,
                                           type=Qt.QueuedConnection)
        web_view.view_created.connect(self.on_view_created)

        self.back_action = page.action(QWebEnginePage.Back)
        self.back_action.setIcon(aa.back.icon)
        self.back_action.setText(aa.back.text)
        self.forward_action = page.action(QWebEnginePage.Forward)
        self.forward_action.setIcon(aa.forward.icon)
        self.forward_action.setText(aa.forward.text)

        self.open_action = a = QAction(aa.open.icon, aa.open.text, self)
        self.open_menu = m = QMenu(self)
        a.setMenu(m)
        m.aboutToShow.connect(self.populate_open_menu)
        connect_lambda(a.triggered, self,
                       lambda self: self.open_book_at_path.emit(None))
        self.copy_action = shortcut_action('copy')
        self.increase_font_size_action = shortcut_action('increase_font_size')
        self.decrease_font_size_action = shortcut_action('decrease_font_size')
        self.fullscreen_action = shortcut_action('fullscreen')

        self.next_action = shortcut_action('next')
        self.previous_action = shortcut_action('previous')
        self.next_section_action = shortcut_action('next_section')
        self.previous_section_action = shortcut_action('previous_section')

        self.search_action = a = toggle_search_action
        a.setText(aa.search.text), a.setIcon(aa.search.icon)
        self.toc_action = a = shortcut_action('toc')
        a.setCheckable(True)
        self.bookmarks_action = a = shortcut_action('bookmarks')
        a.setCheckable(True)
        self.reference_action = a = shortcut_action('reference')
        a.setCheckable(True)
        self.toggle_highlights_action = self.highlights_action = a = shortcut_action(
            'toggle_highlights')
        a.setCheckable(True)
        self.lookup_action = a = shortcut_action('lookup')
        a.setCheckable(True)
        self.inspector_action = a = shortcut_action('inspector')
        a.setCheckable(True)
        self.autoscroll_action = a = shortcut_action('autoscroll')
        a.setCheckable(True)
        self.update_autoscroll_action(False)
        self.chrome_action = shortcut_action('chrome')

        self.mode_action = a = shortcut_action('mode')
        a.setCheckable(True)
        self.print_action = shortcut_action('print')
        self.preferences_action = shortcut_action('preferences')
        self.metadata_action = shortcut_action('metadata')
        self.update_mode_action()
        self.color_scheme_action = a = QAction(aa.color_scheme.icon,
                                               aa.color_scheme.text, self)
        self.color_scheme_menu = m = QMenu(self)
        a.setMenu(m)
        m.aboutToShow.connect(self.populate_color_scheme_menu)

        self.add_actions()

    def add_actions(self):
        self.clear()
        actions = current_actions()
        for x in actions:
            if x is None:
                self.addSeparator()
            else:
                try:
                    self.addAction(getattr(self, '{}_action'.format(x)))
                except AttributeError:
                    pass
        w = self.widgetForAction(self.color_scheme_action)
        if w:
            w.setPopupMode(w.InstantPopup)

    def update_mode_action(self):
        mode = get_session_pref('read_mode', default='paged', group=None)
        a = self.mode_action
        if mode == 'paged':
            a.setChecked(False)
            a.setToolTip(
                _('Switch to flow mode -- where the text is not broken into pages'
                  ))
        else:
            a.setChecked(True)
            a.setToolTip(
                _('Switch to paged mode -- where the text is broken into pages'
                  ))

    def update_autoscroll_action(self, active):
        self.autoscroll_action.setChecked(active)
        self.autoscroll_action.setToolTip(
            _('Turn off auto-scrolling'
              ) if active else _('Turn on auto-scrolling'))

    def update_reference_mode_action(self, enabled):
        self.reference_action.setChecked(enabled)

    def update_dock_actions(self, visibility_map):
        for k in ('toc', 'bookmarks', 'lookup', 'inspector', 'highlights'):
            ac = getattr(self, '{}_action'.format(k))
            ac.setChecked(visibility_map[k])

    def set_tooltips(self, rmap):
        for sc, a in iteritems(self.shortcut_actions):
            if a.isCheckable():
                continue
            x = rmap.get(sc)
            if x is not None:

                def as_text(idx):
                    return index_to_key_sequence(idx).toString(
                        QKeySequence.NativeText)

                keys = sorted(filter(None, map(as_text, x)))
                if keys:
                    a.setToolTip('{} [{}]'.format(a.text(), ', '.join(keys)))

    def populate_open_menu(self):
        m = self.open_menu
        m.clear()
        recent = get_session_pref('standalone_recently_opened',
                                  group=None,
                                  default=())
        if recent:
            for entry in recent:
                try:
                    path = os.path.abspath(entry['pathtoebook'])
                except Exception:
                    continue
                if hasattr(set_book_path,
                           'pathtoebook') and path == os.path.abspath(
                               set_book_path.pathtoebook):
                    continue
                m.addAction('{}\t {}'.format(
                    elided_text(entry['title'], pos='right', width=250),
                    elided_text(os.path.basename(path),
                                width=250))).triggered.connect(
                                    partial(self.open_book_at_path.emit, path))

    def on_view_created(self, data):
        self.default_color_schemes = data['default_color_schemes']

    def populate_color_scheme_menu(self):
        m = self.color_scheme_menu
        m.clear()
        ccs = get_session_pref('current_color_scheme', group=None) or ''
        ucs = get_session_pref('user_color_schemes', group=None) or {}

        def add_action(key, defns):
            a = m.addAction(defns[key]['name'])
            a.setCheckable(True)
            a.setObjectName('color-switch-action:{}'.format(key))
            a.triggered.connect(self.color_switch_triggerred)
            if key == ccs:
                a.setChecked(True)

        for key in sorted(ucs, key=lambda x: primary_sort_key(ucs[x]['name'])):
            add_action(key, ucs)
        m.addSeparator()
        for key in sorted(self.default_color_schemes,
                          key=lambda x: primary_sort_key(
                              self.default_color_schemes[x]['name'])):
            add_action(key, self.default_color_schemes)

    def color_switch_triggerred(self):
        key = self.sender().objectName().partition(':')[-1]
        self.action_triggered.emit('switch_color_scheme:' + key)

    def update_visibility(self):
        self.setVisible(
            bool(get_session_pref('show_actions_toolbar', default=False)))

    @property
    def visible_in_fullscreen(self):
        return bool(
            get_session_pref('show_actions_toolbar_in_fullscreen',
                             default=False))

    def customize(self):
        d = ConfigureToolBar(parent=self.parent())
        if d.exec_() == d.Accepted:
            self.add_actions()
示例#23
0
class Signal(QObject):

    update_found = pyqtSignal(object, object)
示例#24
0
class TextEdit(PlainTextEdit):

    link_clicked = pyqtSignal(object)
    smart_highlighting_updated = pyqtSignal()

    def __init__(self, parent=None, expected_geometry=(100, 50)):
        PlainTextEdit.__init__(self, parent)
        self.gutter_width = 0
        self.expected_geometry = expected_geometry
        self.saved_matches = {}
        self.smarts = NullSmarts(self)
        self.current_cursor_line = None
        self.current_search_mark = None
        self.smarts_highlight_timer = t = QTimer()
        t.setInterval(750), t.setSingleShot(True), t.timeout.connect(
            self.update_extra_selections)
        self.highlighter = SyntaxHighlighter()
        self.line_number_area = LineNumbers(self)
        self.apply_settings()
        self.setMouseTracking(True)
        self.cursorPositionChanged.connect(self.highlight_cursor_line)
        self.blockCountChanged[int].connect(self.update_line_number_area_width)
        self.updateRequest.connect(self.update_line_number_area)
        self.syntax = None

    @dynamic_property
    def is_modified(self):
        ''' True if the document has been modified since it was loaded or since
        the last time is_modified was set to False. '''
        def fget(self):
            return self.document().isModified()

        def fset(self, val):
            self.document().setModified(bool(val))

        return property(fget=fget, fset=fset)

    def sizeHint(self):
        return self.size_hint

    def apply_settings(self, prefs=None, dictionaries_changed=False):  # {{{
        prefs = prefs or tprefs
        self.setLineWrapMode(
            QPlainTextEdit.WidgetWidth
            if prefs['editor_line_wrap'] else QPlainTextEdit.NoWrap)
        theme = get_theme(prefs['editor_theme'])
        self.apply_theme(theme)
        w = self.fontMetrics()
        self.space_width = w.width(' ')
        self.setTabStopWidth(prefs['editor_tab_stop_width'] * self.space_width)
        if dictionaries_changed:
            self.highlighter.rehighlight()

    def apply_theme(self, theme):
        self.theme = theme
        pal = self.palette()
        pal.setColor(pal.Base, theme_color(theme, 'Normal', 'bg'))
        pal.setColor(pal.AlternateBase, theme_color(theme, 'CursorLine', 'bg'))
        pal.setColor(pal.Text, theme_color(theme, 'Normal', 'fg'))
        pal.setColor(pal.Highlight, theme_color(theme, 'Visual', 'bg'))
        pal.setColor(pal.HighlightedText, theme_color(theme, 'Visual', 'fg'))
        self.setPalette(pal)
        self.tooltip_palette = pal = QPalette()
        pal.setColor(pal.ToolTipBase, theme_color(theme, 'Tooltip', 'bg'))
        pal.setColor(pal.ToolTipText, theme_color(theme, 'Tooltip', 'fg'))
        self.line_number_palette = pal = QPalette()
        pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg'))
        pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg'))
        pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg'))
        self.match_paren_format = theme_format(theme, 'MatchParen')
        font = self.font()
        ff = tprefs['editor_font_family']
        if ff is None:
            ff = default_font_family()
        font.setFamily(ff)
        font.setPointSize(tprefs['editor_font_size'])
        self.tooltip_font = QFont(font)
        self.tooltip_font.setPointSize(font.pointSize() - 1)
        self.setFont(font)
        self.highlighter.apply_theme(theme)
        w = self.fontMetrics()
        self.number_width = max(map(lambda x: w.width(str(x)), xrange(10)))
        self.size_hint = QSize(
            self.expected_geometry[0] * w.averageCharWidth(),
            self.expected_geometry[1] * w.height())
        self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg')
        self.highlight_cursor_line()

    # }}}

    def load_text(self,
                  text,
                  syntax='html',
                  process_template=False,
                  doc_name=None):
        self.syntax = syntax
        self.highlighter = get_highlighter(syntax)()
        self.highlighter.apply_theme(self.theme)
        self.highlighter.set_document(self.document(), doc_name=doc_name)
        sclass = {
            'html': HTMLSmarts,
            'xml': HTMLSmarts,
            'css': CSSSmarts
        }.get(syntax, None)
        if sclass is not None:
            self.smarts = sclass(self)
        self.setPlainText(unicodedata.normalize('NFC', text))
        if process_template and QPlainTextEdit.find(self, '%CURSOR%'):
            c = self.textCursor()
            c.insertText('')

    def change_document_name(self, newname):
        self.highlighter.doc_name = newname
        self.highlighter.rehighlight(
        )  # Ensure links are checked w.r.t. the new name correctly

    def replace_text(self, text):
        c = self.textCursor()
        pos = c.position()
        c.beginEditBlock()
        c.clearSelection()
        c.select(c.Document)
        c.insertText(unicodedata.normalize('NFC', text))
        c.endEditBlock()
        c.setPosition(min(pos, len(text)))
        self.setTextCursor(c)
        self.ensureCursorVisible()

    def simple_replace(self, text):
        c = self.textCursor()
        c.insertText(unicodedata.normalize('NFC', text))
        self.setTextCursor(c)

    def go_to_line(self, lnum, col=None):
        lnum = max(1, min(self.blockCount(), lnum))
        c = self.textCursor()
        c.clearSelection()
        c.movePosition(c.Start)
        c.movePosition(c.NextBlock, n=lnum - 1)
        c.movePosition(c.StartOfLine)
        c.movePosition(c.EndOfLine, c.KeepAnchor)
        text = unicode(c.selectedText()).rstrip('\0')
        if col is None:
            c.movePosition(c.StartOfLine)
            lt = text.lstrip()
            if text and lt and lt != text:
                c.movePosition(c.NextWord)
        else:
            c.setPosition(c.block().position() + col)
            if c.blockNumber() + 1 > lnum:
                # We have moved past the end of the line
                c.setPosition(c.block().position())
                c.movePosition(c.EndOfBlock)
        self.setTextCursor(c)
        self.ensureCursorVisible()

    def update_extra_selections(self, instant=True):
        sel = []
        if self.current_cursor_line is not None:
            sel.append(self.current_cursor_line)
        if self.current_search_mark is not None:
            sel.append(self.current_search_mark)
        if instant and not self.highlighter.has_requests:
            sel.extend(self.smarts.get_extra_selections(self))
            self.smart_highlighting_updated.emit()
        else:
            self.smarts_highlight_timer.start()
        self.setExtraSelections(sel)

    # Search and replace {{{
    def mark_selected_text(self):
        sel = QTextEdit.ExtraSelection()
        sel.format.setBackground(self.highlight_color)
        sel.cursor = self.textCursor()
        if sel.cursor.hasSelection():
            self.current_search_mark = sel
            c = self.textCursor()
            c.clearSelection()
            self.setTextCursor(c)
        else:
            self.current_search_mark = None
        self.update_extra_selections()

    def find_in_marked(self, pat, wrap=False, save_match=None):
        if self.current_search_mark is None:
            return False
        csm = self.current_search_mark.cursor
        reverse = pat.flags & regex.REVERSE
        c = self.textCursor()
        c.clearSelection()
        m_start = min(csm.position(), csm.anchor())
        m_end = max(csm.position(), csm.anchor())
        if c.position() < m_start:
            c.setPosition(m_start)
        if c.position() > m_end:
            c.setPosition(m_end)
        pos = m_start if reverse else m_end
        if wrap:
            pos = m_end if reverse else m_start
        c.setPosition(pos, c.KeepAnchor)
        raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR,
                                                '\n').rstrip('\0')
        m = pat.search(raw)
        if m is None:
            return False
        start, end = m.span()
        if start == end:
            return False
        if wrap:
            if reverse:
                textpos = c.anchor()
                start, end = textpos + end, textpos + start
            else:
                start, end = m_start + start, m_start + end
        else:
            if reverse:
                start, end = m_start + end, m_start + start
            else:
                start, end = c.anchor() + start, c.anchor() + end

        c.clearSelection()
        c.setPosition(start)
        c.setPosition(end, c.KeepAnchor)
        self.setTextCursor(c)
        # Center search result on screen
        self.centerCursor()
        if save_match is not None:
            self.saved_matches[save_match] = (pat, m)
        return True

    def all_in_marked(self, pat, template=None):
        if self.current_search_mark is None:
            return 0
        c = self.current_search_mark.cursor
        raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR,
                                                '\n').rstrip('\0')
        if template is None:
            count = len(pat.findall(raw))
        else:
            raw, count = pat.subn(template, raw)
            if count > 0:
                start_pos = min(c.anchor(), c.position())
                c.insertText(raw)
                end_pos = max(c.anchor(), c.position())
                c.setPosition(start_pos), c.setPosition(end_pos, c.KeepAnchor)
                self.update_extra_selections()
        return count

    def find(self,
             pat,
             wrap=False,
             marked=False,
             complete=False,
             save_match=None):
        if marked:
            return self.find_in_marked(pat, wrap=wrap, save_match=save_match)
        reverse = pat.flags & regex.REVERSE
        c = self.textCursor()
        c.clearSelection()
        if complete:
            # Search the entire text
            c.movePosition(c.End if reverse else c.Start)
        pos = c.Start if reverse else c.End
        if wrap and not complete:
            pos = c.End if reverse else c.Start
        c.movePosition(pos, c.KeepAnchor)
        raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR,
                                                '\n').rstrip('\0')
        m = pat.search(raw)
        if m is None:
            return False
        start, end = m.span()
        if start == end:
            return False
        if wrap and not complete:
            if reverse:
                textpos = c.anchor()
                start, end = textpos + end, textpos + start
        else:
            if reverse:
                # Put the cursor at the start of the match
                start, end = end, start
            else:
                textpos = c.anchor()
                start, end = textpos + start, textpos + end
        c.clearSelection()
        c.setPosition(start)
        c.setPosition(end, c.KeepAnchor)
        self.setTextCursor(c)
        # Center search result on screen
        self.centerCursor()
        if save_match is not None:
            self.saved_matches[save_match] = (pat, m)
        return True

    def find_spell_word(self,
                        original_words,
                        lang,
                        from_cursor=True,
                        center_on_cursor=True):
        c = self.textCursor()
        c.setPosition(c.position())
        if not from_cursor:
            c.movePosition(c.Start)
        c.movePosition(c.End, c.KeepAnchor)

        def find_first_word(haystack):
            match_pos, match_word = -1, None
            for w in original_words:
                idx = index_of(w, haystack, lang=lang)
                if idx > -1 and (match_pos == -1 or match_pos > idx):
                    match_pos, match_word = idx, w
            return match_pos, match_word

        while True:
            text = unicode(c.selectedText()).rstrip('\0')
            idx, word = find_first_word(text)
            if idx == -1:
                return False
            c.setPosition(c.anchor() + idx)
            c.setPosition(c.position() + string_length(word), c.KeepAnchor)
            if self.smarts.verify_for_spellcheck(c, self.highlighter):
                self.setTextCursor(c)
                if center_on_cursor:
                    self.centerCursor()
                return True
            c.setPosition(c.position())
            c.movePosition(c.End, c.KeepAnchor)

        return False

    def find_next_spell_error(self, from_cursor=True):
        c = self.textCursor()
        if not from_cursor:
            c.movePosition(c.Start)
        block = c.block()
        while block.isValid():
            for r in block.layout().additionalFormats():
                if r.format.property(SPELL_PROPERTY):
                    if not from_cursor or block.position(
                    ) + r.start + r.length > c.position():
                        c.setPosition(block.position() + r.start)
                        c.setPosition(c.position() + r.length, c.KeepAnchor)
                        self.setTextCursor(c)
                        return True
            block = block.next()
        return False

    def replace(self, pat, template, saved_match='gui'):
        c = self.textCursor()
        raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR,
                                                '\n').rstrip('\0')
        m = pat.fullmatch(raw)
        if m is None:
            # This can happen if either the user changed the selected text or
            # the search expression uses lookahead/lookbehind operators. See if
            # the saved match matches the currently selected text and
            # use it, if so.
            if saved_match is not None and saved_match in self.saved_matches:
                saved_pat, saved = self.saved_matches.pop(saved_match)
                if saved_pat == pat and saved.group() == raw:
                    m = saved
        if m is None:
            return False
        text = m.expand(template)
        c.insertText(text)
        return True

    def go_to_anchor(self, anchor):
        if anchor is TOP:
            c = self.textCursor()
            c.movePosition(c.Start)
            self.setTextCursor(c)
            return True
        base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor)
        raw = unicode(self.toPlainText())
        m = regex.search(base % 'id', raw)
        if m is None:
            m = regex.search(base % 'name', raw)
        if m is not None:
            c = self.textCursor()
            c.setPosition(m.start())
            self.setTextCursor(c)
            return True
        return False

    # }}}

    # Line numbers and cursor line {{{
    def highlight_cursor_line(self):
        sel = QTextEdit.ExtraSelection()
        sel.format.setBackground(self.palette().alternateBase())
        sel.format.setProperty(QTextFormat.FullWidthSelection, True)
        sel.cursor = self.textCursor()
        sel.cursor.clearSelection()
        self.current_cursor_line = sel
        self.update_extra_selections(instant=False)
        # Update the cursor line's line number in the line number area
        try:
            self.line_number_area.update(0, self.last_current_lnum[0],
                                         self.line_number_area.width(),
                                         self.last_current_lnum[1])
        except AttributeError:
            pass
        block = self.textCursor().block()
        top = int(
            self.blockBoundingGeometry(block).translated(
                self.contentOffset()).top())
        height = int(self.blockBoundingRect(block).height())
        self.line_number_area.update(0, top, self.line_number_area.width(),
                                     height)

    def update_line_number_area_width(self, block_count=0):
        self.gutter_width = self.line_number_area_width()
        self.setViewportMargins(self.gutter_width, 0, 0, 0)

    def line_number_area_width(self):
        digits = 1
        limit = max(1, self.blockCount())
        while limit >= 10:
            limit /= 10
            digits += 1

        return 8 + self.number_width * digits

    def update_line_number_area(self, rect, dy):
        if dy:
            self.line_number_area.scroll(0, dy)
        else:
            self.line_number_area.update(0, rect.y(),
                                         self.line_number_area.width(),
                                         rect.height())
        if rect.contains(self.viewport().rect()):
            self.update_line_number_area_width()

    def resizeEvent(self, ev):
        QPlainTextEdit.resizeEvent(self, ev)
        cr = self.contentsRect()
        self.line_number_area.setGeometry(
            QRect(cr.left(), cr.top(), self.line_number_area_width(),
                  cr.height()))

    def paint_line_numbers(self, ev):
        painter = QPainter(self.line_number_area)
        painter.fillRect(ev.rect(),
                         self.line_number_palette.color(QPalette.Base))

        block = self.firstVisibleBlock()
        num = block.blockNumber()
        top = int(
            self.blockBoundingGeometry(block).translated(
                self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        current = self.textCursor().block().blockNumber()
        painter.setPen(self.line_number_palette.color(QPalette.Text))

        while block.isValid() and top <= ev.rect().bottom():
            if block.isVisible() and bottom >= ev.rect().top():
                if current == num:
                    painter.save()
                    painter.setPen(
                        self.line_number_palette.color(QPalette.BrightText))
                    f = QFont(self.font())
                    f.setBold(True)
                    painter.setFont(f)
                    self.last_current_lnum = (top, bottom - top)
                painter.drawText(0, top,
                                 self.line_number_area.width() - 5,
                                 self.fontMetrics().height(), Qt.AlignRight,
                                 str(num + 1))
                if current == num:
                    painter.restore()
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            num += 1

    # }}}

    def event(self, ev):
        if ev.type() == ev.ToolTip:
            self.show_tooltip(ev)
            return True
        if ev.type() == ev.ShortcutOverride:
            if ev in (
                    # Let the global cut/copy/paste/undo/redo shortcuts work,this avoids the nbsp
                    # problem as well, since they use the overridden copy() method
                    # instead of the one from Qt, and allows proper customization
                    # of the shortcuts
                    QKeySequence.Copy,
                    QKeySequence.Cut,
                    QKeySequence.Paste,
                    QKeySequence.Undo,
                    QKeySequence.Redo
            ) or (
                    # This is used to convert typed hex codes into unicode
                    # characters
                    ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier):
                ev.ignore()
                return False
        return QPlainTextEdit.event(self, ev)

    def text_for_range(self, block, r):
        c = self.textCursor()
        c.setPosition(block.position() + r.start)
        c.setPosition(c.position() + r.length, c.KeepAnchor)
        return unicode(c.selectedText())

    def spellcheck_locale_for_cursor(self, c):
        with store_locale:
            formats = self.highlighter.parse_single_block(c.block())[0]
        pos = c.positionInBlock()
        for r in formats:
            if r.start <= pos < r.start + r.length and r.format.property(
                    SPELL_PROPERTY):
                return r.format.property(SPELL_LOCALE_PROPERTY)

    def recheck_word(self, word, locale):
        c = self.textCursor()
        c.movePosition(c.Start)
        block = c.block()
        while block.isValid():
            for r in block.layout().additionalFormats():
                if r.format.property(SPELL_PROPERTY) and self.text_for_range(
                        block, r) == word:
                    self.highlighter.reformat_block(block)
                    break
            block = block.next()

    # Tooltips {{{
    def syntax_range_for_cursor(self, cursor):
        if cursor.isNull():
            return
        pos = cursor.positionInBlock()
        for r in cursor.block().layout().additionalFormats():
            if r.start <= pos < r.start + r.length and r.format.property(
                    SYNTAX_PROPERTY):
                return r

    def syntax_format_for_cursor(self, cursor):
        return getattr(self.syntax_range_for_cursor(cursor), 'format', None)

    def show_tooltip(self, ev):
        c = self.cursorForPosition(ev.pos())
        fmt = self.syntax_format_for_cursor(c)
        if fmt is not None:
            tt = unicode(fmt.toolTip())
            if tt:
                QToolTip.setFont(self.tooltip_font)
                QToolTip.setPalette(self.tooltip_palette)
                QToolTip.showText(ev.globalPos(), textwrap.fill(tt))
                return
        QToolTip.hideText()
        ev.ignore()

    # }}}

    def link_for_position(self, pos):
        c = self.cursorForPosition(pos)
        r = self.syntax_range_for_cursor(c)
        if r is not None and r.format.property(LINK_PROPERTY):
            return self.text_for_range(c.block(), r)

    def mousePressEvent(self, ev):
        if ev.modifiers() & Qt.CTRL:
            url = self.link_for_position(ev.pos())
            if url is not None:
                ev.accept()
                self.link_clicked.emit(url)
                return
        return PlainTextEdit.mousePressEvent(self, ev)

    def get_range_inside_tag(self):
        c = self.textCursor()
        left = min(c.anchor(), c.position())
        right = max(c.anchor(), c.position())
        # For speed we use QPlainTextEdit's toPlainText as we dont care about
        # spaces in this context
        raw = unicode(QPlainTextEdit.toPlainText(self))
        # Make sure the left edge is not within a <>
        gtpos = raw.find('>', left)
        ltpos = raw.find('<', left)
        if gtpos < ltpos:
            left = gtpos + 1 if gtpos > -1 else left
        right = max(left, right)
        if right != left:
            gtpos = raw.find('>', right)
            ltpos = raw.find('<', right)
            if ltpos > gtpos:
                ltpos = raw.rfind('<', left, right + 1)
                right = max(ltpos, left)
        return left, right

    def format_text(self, formatting):
        if self.syntax != 'html':
            return
        if formatting.startswith('justify_'):
            return self.smarts.set_text_alignment(
                self,
                formatting.partition('_')[-1])
        color = 'currentColor'
        if formatting in {'color', 'background-color'}:
            color = QColorDialog.getColor(
                QColor(Qt.black if formatting == 'color' else Qt.white), self,
                _('Choose color'), QColorDialog.ShowAlphaChannel)
            if not color.isValid():
                return
            r, g, b, a = color.getRgb()
            if a == 255:
                color = 'rgb(%d, %d, %d)' % (r, g, b)
            else:
                color = 'rgba(%d, %d, %d, %.2g)' % (r, g, b, a / 255)
        prefix, suffix = {
            'bold': ('<b>', '</b>'),
            'italic': ('<i>', '</i>'),
            'underline': ('<u>', '</u>'),
            'strikethrough': ('<strike>', '</strike>'),
            'superscript': ('<sup>', '</sup>'),
            'subscript': ('<sub>', '</sub>'),
            'color': ('<span style="color: %s">' % color, '</span>'),
            'background-color':
            ('<span style="background-color: %s">' % color, '</span>'),
        }[formatting]
        left, right = self.get_range_inside_tag()
        c = self.textCursor()
        c.setPosition(left)
        c.setPosition(right, c.KeepAnchor)
        prev_text = unicode(c.selectedText()).rstrip('\0')
        c.insertText(prefix + prev_text + suffix)
        if prev_text:
            right = c.position()
            c.setPosition(left)
            c.setPosition(right, c.KeepAnchor)
        else:
            c.setPosition(c.position() - len(suffix))
        self.setTextCursor(c)

    def insert_image(self, href):
        c = self.textCursor()
        template, alt = 'url(%s)', ''
        left = min(c.position(), c.anchor)
        if self.syntax == 'html':
            left, right = self.get_range_inside_tag()
            c.setPosition(left)
            c.setPosition(right, c.KeepAnchor)
            alt = _('Image')
            template = '<img alt="{0}" src="%s" />'.format(alt)
            href = prepare_string_for_xml(href, True)
        text = template % href
        c.insertText(text)
        if self.syntax == 'html':
            c.setPosition(left + 10)
            c.setPosition(c.position() + len(alt), c.KeepAnchor)
        else:
            c.setPosition(left)
            c.setPosition(left + len(text), c.KeepAnchor)
        self.setTextCursor(c)

    def insert_hyperlink(self, target, text):
        if hasattr(self.smarts, 'insert_hyperlink'):
            self.smarts.insert_hyperlink(self, target, text)

    def insert_tag(self, tag):
        if hasattr(self.smarts, 'insert_tag'):
            self.smarts.insert_tag(self, tag)

    def keyPressEvent(self, ev):
        if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier:
            if self.replace_possible_unicode_sequence():
                ev.accept()
                return
        if ev.key() == Qt.Key_Insert:
            self.setOverwriteMode(self.overwriteMode() ^ True)
            ev.accept()
            return
        if isosx and ev.modifiers() == Qt.ControlModifier and re.search(
                r'[a-zA-Z0-9]+', ev.text()) is not None:
            # For some reason Qt 5 translates Cmd+key into text on OS X
            # https://bugreports.qt-project.org/browse/QTBUG-36281
            ev.setAccepted(False)
            return
        QPlainTextEdit.keyPressEvent(self, ev)
        if (ev.key() == Qt.Key_Semicolon or ';' in unicode(ev.text(
        ))) and tprefs['replace_entities_as_typed'] and self.syntax == 'html':
            self.replace_possible_entity()

    def replace_possible_unicode_sequence(self):
        c = self.textCursor()
        has_selection = c.hasSelection()
        if has_selection:
            text = unicode(c.selectedText()).rstrip('\0')
        else:
            c.setPosition(c.position() - min(c.positionInBlock(), 6),
                          c.KeepAnchor)
            text = unicode(c.selectedText()).rstrip('\0')
        m = re.search(r'[a-fA-F0-9]{2,6}$', text)
        if m is None:
            return False
        text = m.group()
        try:
            num = int(text, 16)
        except ValueError:
            return False
        if num > 0x10ffff or num < 1:
            return False
        end_pos = max(c.anchor(), c.position())
        c.setPosition(end_pos - len(text)), c.setPosition(
            end_pos, c.KeepAnchor)
        c.insertText(safe_chr(num))
        return True

    def replace_possible_entity(self):
        c = self.textCursor()
        c.setPosition(c.position() - min(c.positionInBlock(), 10),
                      c.KeepAnchor)
        text = unicode(c.selectedText()).rstrip('\0')
        m = entity_pat.search(text)
        if m is None:
            return
        ent = m.group()
        repl = xml_entity_to_unicode(m)
        if repl != ent:
            c.setPosition(c.position() + m.start(), c.KeepAnchor)
            c.insertText(repl)

    def select_all(self):
        c = self.textCursor()
        c.clearSelection()
        c.setPosition(0)
        c.movePosition(c.End, c.KeepAnchor)
        self.setTextCursor(c)

    def rename_block_tag(self, new_name):
        if hasattr(self.smarts, 'rename_block_tag'):
            self.smarts.rename_block_tag(self, new_name)

    def current_tag(self, for_position_sync=True):
        return self.smarts.cursor_position_with_sourceline(
            self.textCursor(), for_position_sync=for_position_sync)

    def goto_sourceline(self, sourceline, tags, attribute=None):
        return self.smarts.goto_sourceline(self,
                                           sourceline,
                                           tags,
                                           attribute=attribute)

    def get_tag_contents(self):
        c = self.smarts.get_inner_HTML(self)
        if c is not None:
            return self.selected_text_from_cursor(c)

    def goto_css_rule(self, rule_address, sourceline_address=None):
        from calibre.gui2.tweak_book.editor.smart.css import find_rule
        block = None
        if self.syntax == 'css':
            raw = unicode(self.toPlainText())
            line, col = find_rule(raw, rule_address)
            if line is not None:
                block = self.document().findBlockByNumber(line - 1)
        elif sourceline_address is not None:
            sourceline, tags = sourceline_address
            if self.goto_sourceline(sourceline, tags):
                c = self.textCursor()
                c.setPosition(c.position() + 1)
                self.setTextCursor(c)
                raw = self.get_tag_contents()
                line, col = find_rule(raw, rule_address)
                if line is not None:
                    block = self.document().findBlockByNumber(c.blockNumber() +
                                                              line - 1)

        if block is not None and block.isValid():
            c = self.textCursor()
            c.setPosition(block.position() + col)
            self.setTextCursor(c)

    def change_case(self, action, cursor=None):
        cursor = cursor or self.textCursor()
        text = self.selected_text_from_cursor(cursor)
        text = {
            'lower': lower,
            'upper': upper,
            'capitalize': capitalize,
            'title': titlecase,
            'swap': swapcase
        }[action](text)
        cursor.insertText(text)
        self.setTextCursor(cursor)
示例#25
0
class CacheUpdateThread(Thread, QObject):

    total_changed = pyqtSignal(int)
    update_progress = pyqtSignal(int)
    update_details = pyqtSignal(type(u''))

    def __init__(self, config, seralize_books_function, timeout):
        Thread.__init__(self)
        QObject.__init__(self)

        self.daemon = True
        self.config = config
        self.seralize_books = seralize_books_function
        self.timeout = timeout
        self._run = True

    def abort(self):
        self._run = False

    def run(self):
        url = 'https://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'

        self.update_details.emit(_('Checking last download date.'))
        last_download = self.config.get('last_download', None)
        # Don't update the book list if our cache is less than one week old.
        if last_download and (time.time() - last_download) < 604800:
            return

        self.update_details.emit(_('Downloading book list from MobileRead.'))
        # Download the book list HTML file from MobileRead.
        br = browser()
        raw_data = None
        try:
            with closing(br.open(url, timeout=self.timeout)) as f:
                raw_data = f.read()
        except:
            return

        if not raw_data or not self._run:
            return

        self.update_details.emit(_('Processing books.'))
        # Turn books listed in the HTML file into SearchResults's.
        books = []
        try:
            data = html.fromstring(raw_data)
            raw_books = data.xpath('//ul/li')
            self.total_changed.emit(len(raw_books))

            for i, book_data in enumerate(raw_books):
                self.update_details.emit(
                        _('%(num)s of %(tot)s books processed.') % dict(
                            num=i, tot=len(raw_books)))
                book = SearchResult()
                book.detail_item = ''.join(book_data.xpath('.//a/@href'))
                book.formats = ''.join(book_data.xpath('.//i/text()'))
                book.formats = book.formats.strip()

                text = ''.join(book_data.xpath('.//a/text()'))
                if ':' in text:
                    book.author, q, text = text.partition(':')
                book.author = book.author.strip()
                book.title = text.strip()
                books.append(book)

                if not self._run:
                    books = []
                    break
                else:
                    self.update_progress.emit(i)
        except:
            pass

        # Save the book list and it's create time.
        if books:
            self.config['book_list'] = self.seralize_books(books)
            self.config['last_download'] = time.time()
示例#26
0
class CoverView(QWidget):  # {{{

    cover_changed = pyqtSignal(object, object)
    cover_removed = pyqtSignal(object)
    open_cover_with = pyqtSignal(object, object)
    search_internet = pyqtSignal(object)

    def __init__(self, vertical, parent=None):
        QWidget.__init__(self, parent)
        self._current_pixmap_size = QSize(120, 120)
        self.vertical = vertical

        self.animation = QPropertyAnimation(self, b'current_pixmap_size', self)
        self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
        self.animation.setDuration(1000)
        self.animation.setStartValue(QSize(0, 0))
        self.animation.valueChanged.connect(self.value_changed)

        self.setSizePolicy(
                QSizePolicy.Expanding if vertical else QSizePolicy.Minimum,
                QSizePolicy.Expanding)

        self.default_pixmap = QPixmap(I('default_cover.png'))
        self.pixmap = self.default_pixmap
        self.pwidth = self.pheight = None
        self.data = {}

        self.do_layout()

    def value_changed(self, val):
        self.update()

    def setCurrentPixmapSize(self, val):
        self._current_pixmap_size = val

    def do_layout(self):
        if self.rect().width() == 0 or self.rect().height() == 0:
            return
        pixmap = self.pixmap
        pwidth, pheight = pixmap.width(), pixmap.height()
        try:
            self.pwidth, self.pheight = fit_image(pwidth, pheight,
                            self.rect().width(), self.rect().height())[1:]
        except:
            self.pwidth, self.pheight = self.rect().width()-1, \
                    self.rect().height()-1
        self.current_pixmap_size = QSize(self.pwidth, self.pheight)
        self.animation.setEndValue(self.current_pixmap_size)

    def show_data(self, data):
        self.animation.stop()
        same_item = getattr(data, 'id', True) == self.data.get('id', False)
        self.data = {'id':data.get('id', None)}
        if data.cover_data[1]:
            self.pixmap = QPixmap.fromImage(data.cover_data[1])
            if self.pixmap.isNull() or self.pixmap.width() < 5 or \
                    self.pixmap.height() < 5:
                self.pixmap = self.default_pixmap
        else:
            self.pixmap = self.default_pixmap
        self.do_layout()
        self.update()
        if (not same_item and not config['disable_animations'] and
                self.isVisible()):
            self.animation.start()

    def paintEvent(self, event):
        canvas_size = self.rect()
        width = self.current_pixmap_size.width()
        extrax = canvas_size.width() - width
        if extrax < 0:
            extrax = 0
        x = int(extrax//2)
        height = self.current_pixmap_size.height()
        extray = canvas_size.height() - height
        if extray < 0:
            extray = 0
        y = int(extray//2)
        target = QRect(x, y, width, height)
        p = QPainter(self)
        p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
        try:
            dpr = self.devicePixelRatioF()
        except AttributeError:
            dpr = self.devicePixelRatio()
        spmap = self.pixmap.scaled(target.size() * dpr, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        spmap.setDevicePixelRatio(dpr)
        p.drawPixmap(target, spmap)
        if gprefs['bd_overlay_cover_size']:
            sztgt = target.adjusted(0, 0, 0, -4)
            f = p.font()
            f.setBold(True)
            p.setFont(f)
            sz = '\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height())
            flags = Qt.AlignBottom|Qt.AlignRight|Qt.TextSingleLine
            szrect = p.boundingRect(sztgt, flags, sz)
            p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200))
            p.setPen(QPen(QColor(255,255,255)))
            p.drawText(sztgt, flags, sz)
        p.end()

    current_pixmap_size = pyqtProperty('QSize',
            fget=lambda self: self._current_pixmap_size,
            fset=setCurrentPixmapSize
            )

    def contextMenuEvent(self, ev):
        cm = QMenu(self)
        paste = cm.addAction(_('Paste cover'))
        copy = cm.addAction(_('Copy cover'))
        save = cm.addAction(_('Save cover to disk'))
        remove = cm.addAction(_('Remove cover'))
        gc = cm.addAction(_('Generate cover from metadata'))
        cm.addSeparator()
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)
        save.triggered.connect(self.save_cover)
        create_open_cover_with_menu(self, cm)
        cm.si = m = create_search_internet_menu(self.search_internet.emit)
        cm.addMenu(m)
        cm.exec_(ev.globalPos())

    def open_with(self, entry):
        id_ = self.data.get('id', None)
        if id_ is not None:
            self.open_cover_with.emit(id_, entry)

    def choose_open_with(self):
        from calibre.gui2.open_with import choose_program
        entry = choose_program('cover_image', self)
        if entry is not None:
            self.open_with(entry)

    def copy_to_clipboard(self):
        QApplication.instance().clipboard().setPixmap(self.pixmap)

    def paste_from_clipboard(self, pmap=None):
        if not isinstance(pmap, QPixmap):
            cb = QApplication.instance().clipboard()
            pmap = cb.pixmap()
            if pmap.isNull() and cb.supportsSelection():
                pmap = cb.pixmap(cb.Selection)
        if not pmap.isNull():
            self.update_cover(pmap)

    def save_cover(self):
        from calibre.gui2.ui import get_gui
        book_id = self.data.get('id')
        db = get_gui().current_db.new_api
        path = choose_save_file(
            self, 'save-cover-from-book-details', _('Choose cover save location'),
            filters=[(_('JPEG images'), ['jpg', 'jpeg'])], all_files=False,
            initial_filename='{}.jpeg'.format(sanitize_file_name(db.field_for('title', book_id, default_value='cover')))
        )
        if path:
            db.copy_cover_to(book_id, path)

    def update_cover(self, pmap=None, cdata=None):
        if pmap is None:
            pmap = QPixmap()
            pmap.loadFromData(cdata)
        if pmap.isNull():
            return
        if pmap.hasAlphaChannel():
            pmap = QPixmap.fromImage(blend_image(image_from_x(pmap)))
        self.pixmap = pmap
        self.do_layout()
        self.update()
        self.update_tooltip(getattr(self.parent(), 'current_path', ''))
        if not config['disable_animations']:
            self.animation.start()
        id_ = self.data.get('id', None)
        if id_ is not None:
            self.cover_changed.emit(id_, cdata or pixmap_to_data(pmap))

    def generate_cover(self, *args):
        book_id = self.data.get('id')
        if book_id is not None:
            from calibre.ebooks.covers import generate_cover
            from calibre.gui2.ui import get_gui
            mi = get_gui().current_db.new_api.get_metadata(book_id)
            cdata = generate_cover(mi)
            self.update_cover(cdata=cdata)

    def remove_cover(self):
        id_ = self.data.get('id', None)
        self.pixmap = self.default_pixmap
        self.do_layout()
        self.update()
        if id_ is not None:
            self.cover_removed.emit(id_)

    def update_tooltip(self, current_path):
        try:
            sz = self.pixmap.size()
        except:
            sz = QSize(0, 0)
        self.setToolTip(
            '<p>'+_('Double click to open the Book details window') +
            '<br><br>' + _('Path') + ': ' + current_path +
            '<br><br>' + _('Cover size: %(width)d x %(height)d pixels')%dict(
                width=sz.width(), height=sz.height())
        )
示例#27
0
class Diff(Dialog):

    revert_requested = pyqtSignal()
    line_activated = pyqtSignal(object, object, object)

    def __init__(self, revert_button_msg=None, parent=None, show_open_in_editor=False, show_as_window=False):
        self.context = 3
        self.beautify = False
        self.apply_diff_calls = []
        self.show_open_in_editor = show_open_in_editor
        self.revert_button_msg = revert_button_msg
        Dialog.__init__(self, _('Differences between books'), 'diff-dialog', parent=parent)
        self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMinMaxButtonsHint)
        if show_as_window:
            self.setWindowFlags(Qt.WindowType.Window)
        self.view.line_activated.connect(self.line_activated)

    def sizeHint(self):
        geom = QApplication.instance().desktop().availableGeometry(self)
        return QSize(int(0.9 * geom.width()), int(0.8 * geom.height()))

    def setup_ui(self):
        self.setWindowIcon(QIcon(I('diff.png')))
        self.stacks = st = QStackedLayout(self)
        self.busy = BusyWidget(self)
        self.w = QWidget(self)
        st.addWidget(self.busy), st.addWidget(self.w)

        self.setLayout(st)
        self.l = l = QGridLayout()
        self.w.setLayout(l)

        self.view = v = DiffView(self, show_open_in_editor=self.show_open_in_editor)
        l.addWidget(v, l.rowCount(), 0, 1, -1)

        r = l.rowCount()
        self.bp = b = QToolButton(self)
        b.setIcon(QIcon(I('back.png')))
        connect_lambda(b.clicked, self, lambda self: self.view.next_change(-1))
        b.setToolTip(_('Go to previous change') + ' [p]')
        b.setText(_('&Previous change')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 0)

        self.bn = b = QToolButton(self)
        b.setIcon(QIcon(I('forward.png')))
        connect_lambda(b.clicked, self, lambda self: self.view.next_change(1))
        b.setToolTip(_('Go to next change') + ' [n]')
        b.setText(_('&Next change')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 1)

        self.search = s = HistoryLineEdit2(self)
        s.initialize('diff_search_history')
        l.addWidget(s, r, 2)
        s.setPlaceholderText(_('Search for text'))
        connect_lambda(s.returnPressed, self, lambda self: self.do_search(False))
        self.sbn = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-down.png')))
        connect_lambda(b.clicked, self, lambda self: self.do_search(False))
        b.setToolTip(_('Find next match'))
        b.setText(_('Next &match')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 3)
        self.sbp = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-up.png')))
        connect_lambda(b.clicked, self, lambda self: self.do_search(True))
        b.setToolTip(_('Find previous match'))
        b.setText(_('P&revious match')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 4)
        self.lb = b = QRadioButton(_('Left panel'), self)
        b.setToolTip(_('Perform search in the left panel'))
        l.addWidget(b, r, 5)
        self.rb = b = QRadioButton(_('Right panel'), self)
        b.setToolTip(_('Perform search in the right panel'))
        l.addWidget(b, r, 6)
        b.setChecked(True)
        self.pb = b = QToolButton(self)
        b.setIcon(QIcon(I('config.png')))
        b.setText(_('&Options')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
        b.setToolTip(_('Change how the differences are displayed'))
        b.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
        m = QMenu(b)
        b.setMenu(m)
        cm = self.cm = QMenu(_('Lines of context around each change'))
        for i in (3, 5, 10, 50):
            cm.addAction(_('Show %d lines of context') % i, partial(self.change_context, i))
        cm.addAction(_('Show all text'), partial(self.change_context, None))
        self.beautify_action = m.addAction('', self.toggle_beautify)
        self.set_beautify_action_text()
        m.addMenu(cm)
        l.addWidget(b, r, 7)

        self.hl = QHBoxLayout()
        l.addLayout(self.hl, l.rowCount(), 0, 1, -1)
        self.names = QLabel('')
        self.hl.addWidget(self.names, r)

        self.bb.setStandardButtons(self.bb.Close)
        if self.revert_button_msg is not None:
            self.rvb = b = self.bb.addButton(self.revert_button_msg, self.bb.ActionRole)
            b.setIcon(QIcon(I('edit-undo.png'))), b.setAutoDefault(False)
            b.clicked.connect(self.revert_requested)
            b.clicked.connect(self.reject)
        self.bb.button(self.bb.Close).setDefault(True)
        self.hl.addWidget(self.bb, r)

        self.view.setFocus(Qt.FocusReason.OtherFocusReason)

    def break_cycles(self):
        self.view = None
        for x in ('revert_requested', 'line_activated'):
            try:
                getattr(self, x).disconnect()
            except:
                pass

    def do_search(self, reverse):
        text = unicode_type(self.search.text())
        if not text.strip():
            return
        v = self.view.view.left if self.lb.isChecked() else self.view.view.right
        v.search(text, reverse=reverse)

    def change_context(self, context):
        if context == self.context:
            return
        self.context = context
        self.refresh()

    def refresh(self):
        with self:
            self.view.clear()
            for args, kwargs in self.apply_diff_calls:
                kwargs['context'] = self.context
                kwargs['beautify'] = self.beautify
                self.view.add_diff(*args, **kwargs)
            self.view.finalize()

    def toggle_beautify(self):
        self.beautify = not self.beautify
        self.set_beautify_action_text()
        self.refresh()

    def set_beautify_action_text(self):
        self.beautify_action.setText(
            _('Beautify files before comparing them') if not self.beautify else
            _('Do not beautify files before comparing'))

    def __enter__(self):
        self.stacks.setCurrentIndex(0)
        self.busy.setVisible(True)
        self.busy.pi.startAnimation()
        QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
        QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents | QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers)

    def __exit__(self, *args):
        self.busy.pi.stopAnimation()
        self.stacks.setCurrentIndex(1)
        QApplication.restoreOverrideCursor()

    def set_names(self, names):
        if isinstance(names, tuple):
            self.names.setText('%s <--> %s' % names)
        else:
            self.names.setText('')

    def ebook_diff(self, path1, path2, names=None):
        self.set_names(names)
        with self:
            identical = self.apply_diff(_('The books are identical'), *ebook_diff(path1, path2))
            self.view.finalize()
        if identical:
            self.reject()

    def container_diff(self, left, right, identical_msg=None, names=None):
        self.set_names(names)
        with self:
            identical = self.apply_diff(identical_msg or _('No changes found'), *container_diff(left, right))
            self.view.finalize()
        if identical:
            self.reject()

    def file_diff(self, left, right, identical_msg=None):
        with self:
            identical = self.apply_diff(identical_msg or _('The files are identical'), *file_diff(left, right))
            self.view.finalize()
        if identical:
            self.reject()

    def string_diff(self, left, right, **kw):
        with self:
            identical = self.apply_diff(kw.pop('identical_msg', None) or _('No differences found'), *string_diff(left, right, **kw))
            self.view.finalize()
        if identical:
            self.reject()

    def dir_diff(self, left, right, identical_msg=None):
        with self:
            identical = self.apply_diff(identical_msg or _('The directories are identical'), *dir_diff(left, right))
            self.view.finalize()
        if identical:
            self.reject()

    def apply_diff(self, identical_msg, cache, syntax_map, changed_names, renamed_names, removed_names, added_names):
        self.view.clear()
        self.apply_diff_calls = calls = []

        def add(args, kwargs):
            self.view.add_diff(*args, **kwargs)
            calls.append((args, kwargs))

        if len(changed_names) + len(renamed_names) + len(removed_names) + len(added_names) < 1:
            self.busy.setVisible(False)
            info_dialog(self, _('No changes found'), identical_msg, show=True)
            self.busy.setVisible(True)
            return True

        kwargs = lambda name: {'context':self.context, 'beautify':self.beautify, 'syntax':syntax_map.get(name, None)}

        if isinstance(changed_names, dict):
            for name, other_name in sorted(iteritems(changed_names), key=lambda x:numeric_sort_key(x[0])):
                args = (name, other_name, cache.left(name), cache.right(other_name))
                add(args, kwargs(name))
        else:
            for name in sorted(changed_names, key=numeric_sort_key):
                args = (name, name, cache.left(name), cache.right(name))
                add(args, kwargs(name))

        for name in sorted(added_names, key=numeric_sort_key):
            args = (_('[%s was added]') % name, name, None, cache.right(name))
            add(args, kwargs(name))

        for name in sorted(removed_names, key=numeric_sort_key):
            args = (name, _('[%s was removed]') % name, cache.left(name), None)
            add(args, kwargs(name))

        for name, new_name in sorted(iteritems(renamed_names), key=lambda x:numeric_sort_key(x[0])):
            args = (name, new_name, None, None)
            add(args, kwargs(name))

    def keyPressEvent(self, ev):
        if not self.view.handle_key(ev):
            if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
                return  # The enter key is used by the search box, so prevent it closing the dialog
            if ev.key() == Qt.Key.Key_Slash:
                return self.search.setFocus(Qt.FocusReason.OtherFocusReason)
            if ev.matches(QKeySequence.StandardKey.Copy):
                text = self.view.view.left.selected_text + self.view.view.right.selected_text
                if text:
                    QApplication.clipboard().setText(text)
                return
            if ev.matches(QKeySequence.StandardKey.FindNext):
                self.sbn.click()
                return
            if ev.matches(QKeySequence.StandardKey.FindPrevious):
                self.sbp.click()
                return
            return Dialog.keyPressEvent(self, ev)
示例#28
0
class BookDetails(QWidget):  # {{{

    show_book_info = pyqtSignal()
    open_containing_folder = pyqtSignal(int)
    view_specific_format = pyqtSignal(int, object)
    search_requested = pyqtSignal(object)
    remove_specific_format = pyqtSignal(int, object)
    remove_metadata_item = pyqtSignal(int, object, object)
    save_specific_format = pyqtSignal(int, object)
    restore_specific_format = pyqtSignal(int, object)
    set_cover_from_format = pyqtSignal(int, object)
    compare_specific_format = pyqtSignal(int, object)
    copy_link = pyqtSignal(object)
    remote_file_dropped = pyqtSignal(object, object)
    files_dropped = pyqtSignal(object, object)
    cover_changed = pyqtSignal(object, object)
    open_cover_with = pyqtSignal(object, object)
    cover_removed = pyqtSignal(object)
    view_device_book = pyqtSignal(object)
    manage_category = pyqtSignal(object, object)
    edit_identifiers = pyqtSignal()
    open_fmt_with = pyqtSignal(int, object, object)
    edit_book = pyqtSignal(int, object)
    find_in_tag_browser = pyqtSignal(object, object)

    # Drag 'n drop {{{

    def dragEnterEvent(self, event):
        md = event.mimeData()
        if dnd_has_extension(md, image_extensions() + BOOK_EXTENSIONS, allow_all_extensions=True, allow_remote=True) or \
                dnd_has_image(md):
            event.acceptProposedAction()

    def dropEvent(self, event):
        event.setDropAction(Qt.CopyAction)
        md = event.mimeData()

        image_exts = set(image_extensions()) - set(tweaks['cover_drop_exclude'])
        x, y = dnd_get_image(md, image_exts)
        if x is not None:
            # We have an image, set cover
            event.accept()
            if y is None:
                # Local image
                self.cover_view.paste_from_clipboard(x)
                self.update_layout()
            else:
                self.remote_file_dropped.emit(x, y)
                # We do not support setting cover *and* adding formats for
                # a remote drop, anyway, so return
                return

        # Now look for ebook files
        urls, filenames = dnd_get_files(md, BOOK_EXTENSIONS, allow_all_extensions=True, filter_exts=image_exts)
        if not urls:
            # Nothing found
            return

        if not filenames:
            # Local files
            self.files_dropped.emit(event, urls)
        else:
            # Remote files, use the first file
            self.remote_file_dropped.emit(urls[0], filenames[0])
        event.accept()

    def dragMoveEvent(self, event):
        event.acceptProposedAction()

    # }}}

    def __init__(self, vertical, parent=None):
        QWidget.__init__(self, parent)
        self.last_data = {}
        self.setAcceptDrops(True)
        self._layout = DetailsLayout(vertical, self)
        self.setLayout(self._layout)
        self.current_path = ''

        self.cover_view = CoverView(vertical, self)
        self.cover_view.search_internet.connect(self.search_internet)
        self.cover_view.cover_changed.connect(self.cover_changed.emit)
        self.cover_view.open_cover_with.connect(self.open_cover_with.emit)
        self.cover_view.cover_removed.connect(self.cover_removed.emit)
        self._layout.addWidget(self.cover_view)
        self.book_info = BookInfo(vertical, self)
        self.book_info.show_book_info = self.show_book_info
        self.book_info.search_internet = self.search_internet
        self.book_info.search_requested = self.search_requested.emit
        self._layout.addWidget(self.book_info)
        self.book_info.link_clicked.connect(self.handle_click)
        self.book_info.remove_format.connect(self.remove_specific_format)
        self.book_info.remove_item.connect(self.remove_metadata_item)
        self.book_info.open_fmt_with.connect(self.open_fmt_with)
        self.book_info.edit_book.connect(self.edit_book)
        self.book_info.save_format.connect(self.save_specific_format)
        self.book_info.restore_format.connect(self.restore_specific_format)
        self.book_info.set_cover_format.connect(self.set_cover_from_format)
        self.book_info.compare_format.connect(self.compare_specific_format)
        self.book_info.copy_link.connect(self.copy_link)
        self.book_info.manage_category.connect(self.manage_category)
        self.book_info.find_in_tag_browser.connect(self.find_in_tag_browser)
        self.book_info.edit_identifiers.connect(self.edit_identifiers)
        self.setCursor(Qt.PointingHandCursor)

    def search_internet(self, data):
        if self.last_data:
            if data.author is None:
                url = url_for_book_search(data.where, title=self.last_data['title'], author=self.last_data['authors'][0])
            else:
                url = url_for_author_search(data.where, author=data.author)
            safe_open_url(url)

    def handle_click(self, link):
        typ, val = link.partition(':')[::2]

        def search_term(field, val):
            self.search_requested.emit('{}:"={}"'.format(field, val.replace('"', '\\"')))

        def browse(url):
            try:
                safe_open_url(QUrl(url, QUrl.TolerantMode))
            except Exception:
                import traceback
                traceback.print_exc()

        if typ == 'action':
            data = json_loads(from_hex_bytes(val))
            dt = data['type']
            if dt == 'search':
                search_term(data['term'], data['value'])
            elif dt == 'author':
                url = data['url']
                if url == 'calibre':
                    search_term('authors', data['name'])
                else:
                    browse(url)
            elif dt == 'format':
                book_id, fmt = data['book_id'], data['fmt']
                self.view_specific_format.emit(int(book_id), fmt)
            elif dt == 'identifier':
                if data['url']:
                    browse(data['url'])
            elif dt == 'path':
                self.open_containing_folder.emit(int(data['loc']))
            elif dt == 'devpath':
                self.view_device_book.emit(data['loc'])
        else:
            browse(link)

    def mouseDoubleClickEvent(self, ev):
        ev.accept()
        self.show_book_info.emit()

    def show_data(self, data):
        try:
            self.last_data = {'title':data.title, 'authors':data.authors}
        except Exception:
            self.last_data = {}
        self.book_info.show_data(data)
        self.cover_view.show_data(data)
        self.current_path = getattr(data, 'path', '')
        self.update_layout()

    def update_layout(self):
        self.cover_view.setVisible(gprefs['bd_show_cover'])
        self._layout.do_layout(self.rect())
        self.cover_view.update_tooltip(self.current_path)

    def reset_info(self):
        self.show_data(Metadata(_('Unknown')))
示例#29
0
class ConfigWidgetBase(QWidget, ConfigWidgetInterface):

    '''
    Base class that contains code to easily add standard config widgets like
    checkboxes, combo boxes, text fields and so on. See the :meth:`register`
    method.

    This class automatically handles change notification, resetting to default,
    translation between gui objects and config objects, etc. for registered
    settings.

    If your config widget inherits from this class but includes setting that
    are not registered, you should override the :class:`ConfigWidgetInterface` methods
    and call the base class methods inside the overrides.
    '''

    changed_signal = pyqtSignal()
    restart_now = pyqtSignal()
    supports_restoring_to_defaults = True
    restart_critical = False

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        if hasattr(self, 'setupUi'):
            self.setupUi(self)
        self.settings = {}

    def register(self, name, config_obj, gui_name=None, choices=None,
            restart_required=False, empty_string_is_None=True, setting=Setting):
        '''
        Register a setting.

        :param name: The setting name
        :param config: The config object that reads/writes the setting
        :param gui_name: The name of the GUI object that presents an interface
                         to change the setting. By default it is assumed to be
                         ``'opt_' + name``.
        :param choices: If this setting is a multiple choice (combobox) based
                        setting, the list of choices. The list is a list of two
                        element tuples of the form: ``[(gui name, value), ...]``
        :param setting: The class responsible for managing this setting. The
                        default class handles almost all cases, so this param
                        is rarely used.
        '''
        setting = setting(name, config_obj, self, gui_name=gui_name,
                choices=choices, restart_required=restart_required,
                empty_string_is_None=empty_string_is_None)
        return self.register_setting(setting)

    def register_setting(self, setting):
        self.settings[setting.name] = setting
        return setting

    def initialize(self):
        for setting in self.settings.values():
            setting.initialize()

    def commit(self, *args):
        restart_required = False
        for setting in self.settings.values():
            rr = setting.commit()
            if rr:
                restart_required = True
        return restart_required

    def restore_defaults(self, *args):
        for setting in self.settings.values():
            setting.restore_defaults()
示例#30
0
class Editor(QMainWindow):

    has_line_numbers = True

    modification_state_changed = pyqtSignal(object)
    undo_redo_state_changed = pyqtSignal(object, object)
    copy_available_state_changed = pyqtSignal(object)
    data_changed = pyqtSignal(object)
    cursor_position_changed = pyqtSignal()
    word_ignored = pyqtSignal(object, object)
    link_clicked = pyqtSignal(object)
    smart_highlighting_updated = pyqtSignal()

    def __init__(self, syntax, parent=None):
        QMainWindow.__init__(self, parent)
        if parent is None:
            self.setWindowFlags(Qt.Widget)
        self.is_synced_to_container = False
        self.syntax = syntax
        self.editor = TextEdit(self)
        self.editor.setContextMenuPolicy(Qt.CustomContextMenu)
        self.editor.customContextMenuRequested.connect(self.show_context_menu)
        self.setCentralWidget(self.editor)
        self.create_toolbars()
        self.undo_available = False
        self.redo_available = False
        self.copy_available = self.cut_available = False
        self.editor.modificationChanged.connect(self._modification_state_changed)
        self.editor.undoAvailable.connect(self._undo_available)
        self.editor.redoAvailable.connect(self._redo_available)
        self.editor.textChanged.connect(self._data_changed)
        self.editor.copyAvailable.connect(self._copy_available)
        self.editor.cursorPositionChanged.connect(self._cursor_position_changed)
        self.editor.link_clicked.connect(self.link_clicked)
        self.editor.smart_highlighting_updated.connect(self.smart_highlighting_updated)

    @property
    def current_line(self):
        return self.editor.textCursor().blockNumber()

    @current_line.setter
    def current_line(self, val):
        self.editor.go_to_line(val)

    @property
    def current_editing_state(self):
        c = self.editor.textCursor()
        return {'cursor':(c.anchor(), c.position())}

    @current_editing_state.setter
    def current_editing_state(self, val):
        anchor, position = val.get('cursor', (None, None))
        if anchor is not None and position is not None:
            c = self.editor.textCursor()
            c.setPosition(anchor), c.setPosition(position, c.KeepAnchor)
            self.editor.setTextCursor(c)

    def current_tag(self, for_position_sync=True):
        return self.editor.current_tag(for_position_sync=for_position_sync)

    @property
    def number_of_lines(self):
        return self.editor.blockCount()

    @property
    def data(self):
        ans = self.get_raw_data()
        ans, changed = replace_encoding_declarations(ans, enc='utf-8', limit=4*1024)
        if changed:
            self.data = ans
        return ans.encode('utf-8')

    @data.setter
    def data(self, val):
        self.editor.load_text(val, syntax=self.syntax, doc_name=editor_name(self))

    def init_from_template(self, template):
        self.editor.load_text(template, syntax=self.syntax, process_template=True, doc_name=editor_name(self))

    def change_document_name(self, newname):
        self.editor.change_document_name(newname)
        self.editor.completion_doc_name = newname

    def get_raw_data(self):
        # The EPUB spec requires NFC normalization, see section 1.3.6 of
        # http://www.idpf.org/epub/20/spec/OPS_2.0.1_draft.htm
        return unicodedata.normalize('NFC', unicode_type(self.editor.toPlainText()).rstrip('\0'))

    def replace_data(self, raw, only_if_different=True):
        if isinstance(raw, bytes):
            raw = raw.decode('utf-8')
        current = self.get_raw_data() if only_if_different else False
        if current != raw:
            self.editor.replace_text(raw)

    def apply_settings(self, prefs=None, dictionaries_changed=False):
        self.editor.apply_settings(prefs=None, dictionaries_changed=dictionaries_changed)

    def set_focus(self):
        self.editor.setFocus(Qt.OtherFocusReason)

    def action_triggered(self, action):
        action, args = action[0], action[1:]
        func = getattr(self.editor, action)
        func(*args)

    def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False, width=-1, height=-1):
        self.editor.insert_image(href, fullpage=fullpage, preserve_aspect_ratio=preserve_aspect_ratio, width=width, height=height)

    def insert_hyperlink(self, href, text, template=None):
        self.editor.insert_hyperlink(href, text, template=template)

    def _build_insert_tag_button_menu(self):
        m = self.insert_tag_menu
        m.clear()
        names = tprefs['insert_tag_mru']
        for name in names:
            m.addAction(name, partial(self.insert_tag, name))
        if names:
            m.addSeparator()
            m = m.addMenu(_('Remove from this menu'))
            for name in names:
                m.addAction(name, partial(self.remove_insert_tag, name))

    def insert_tag(self, name):
        self.editor.insert_tag(name)
        mru = tprefs['insert_tag_mru']
        try:
            mru.remove(name)
        except ValueError:
            pass
        mru.insert(0, name)
        tprefs['insert_tag_mru'] = mru
        self._build_insert_tag_button_menu()

    def remove_insert_tag(self, name):
        mru = tprefs['insert_tag_mru']
        try:
            mru.remove(name)
        except ValueError:
            pass
        tprefs['insert_tag_mru'] = mru
        self._build_insert_tag_button_menu()

    def set_request_completion(self, callback=None, doc_name=None):
        self.editor.request_completion = callback
        self.editor.completion_doc_name = doc_name

    def handle_completion_result(self, result):
        return self.editor.handle_completion_result(result)

    def undo(self):
        self.editor.undo()

    def redo(self):
        self.editor.redo()

    @property
    def selected_text(self):
        return self.editor.selected_text

    def get_smart_selection(self, update=True):
        return self.editor.smarts.get_smart_selection(self.editor, update=update)

    # Search and replace {{{
    def mark_selected_text(self):
        self.editor.mark_selected_text()

    def find(self, *args, **kwargs):
        return self.editor.find(*args, **kwargs)

    def find_text(self, *args, **kwargs):
        return self.editor.find_text(*args, **kwargs)

    def find_spell_word(self, *args, **kwargs):
        return self.editor.find_spell_word(*args, **kwargs)

    def replace(self, *args, **kwargs):
        return self.editor.replace(*args, **kwargs)

    def all_in_marked(self, *args, **kwargs):
        return self.editor.all_in_marked(*args, **kwargs)

    def go_to_anchor(self, *args, **kwargs):
        return self.editor.go_to_anchor(*args, **kwargs)
    # }}}

    @property
    def has_marked_text(self):
        return self.editor.current_search_mark is not None

    @property
    def is_modified(self):
        return self.editor.is_modified

    @is_modified.setter
    def is_modified(self, val):
        self.editor.is_modified = val

    def create_toolbars(self):
        self.action_bar = b = self.addToolBar(_('Edit actions tool bar'))
        b.setObjectName('action_bar')  # Needed for saveState
        self.tools_bar = b = self.addToolBar(_('Editor tools'))
        b.setObjectName('tools_bar')
        self.bars = [self.action_bar, self.tools_bar]
        if self.syntax == 'html':
            self.format_bar = b = self.addToolBar(_('Format text'))
            b.setObjectName('html_format_bar')
            self.bars.append(self.format_bar)
        self.insert_tag_menu = QMenu(self)
        self.populate_toolbars()
        for x in self.bars:
            x.setFloatable(False)
            x.topLevelChanged.connect(self.toolbar_floated)
            x.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size']))

    def toolbar_floated(self, floating):
        if not floating:
            self.save_state()
            for ed in itervalues(editors):
                if ed is not self:
                    ed.restore_state()

    def save_state(self):
        for bar in self.bars:
            if bar.isFloating():
                return
        tprefs['%s-editor-state' % self.syntax] = bytearray(self.saveState())

    def restore_state(self):
        state = tprefs.get('%s-editor-state' % self.syntax, None)
        if state is not None:
            self.restoreState(state)
        for bar in self.bars:
            bar.setVisible(len(bar.actions()) > 0)

    def populate_toolbars(self):
        self.action_bar.clear(), self.tools_bar.clear()

        def add_action(name, bar):
            if name is None:
                bar.addSeparator()
                return
            try:
                ac = actions[name]
            except KeyError:
                if DEBUG:
                    prints('Unknown editor tool: %r' % name)
                return
            bar.addAction(ac)
            if name == 'insert-tag':
                w = bar.widgetForAction(ac)
                if hasattr(w, 'setPopupMode'):
                    # For some unknown reason this button is occassionally a
                    # QPushButton instead of a QToolButton
                    w.setPopupMode(QToolButton.MenuButtonPopup)
                w.setMenu(self.insert_tag_menu)
                w.setContextMenuPolicy(Qt.CustomContextMenu)
                w.customContextMenuRequested.connect(w.showMenu)
                self._build_insert_tag_button_menu()
            elif name == 'change-paragraph':
                m = ac.m = QMenu()
                ac.setMenu(m)
                ch = bar.widgetForAction(ac)
                if hasattr(ch, 'setPopupMode'):
                    # For some unknown reason this button is occassionally a
                    # QPushButton instead of a QToolButton
                    ch.setPopupMode(QToolButton.InstantPopup)
                for name in tuple('h%d' % d for d in range(1, 7)) + ('p',):
                    m.addAction(actions['rename-block-tag-%s' % name])

        for name in tprefs.get('editor_common_toolbar', ()):
            add_action(name, self.action_bar)

        for name in tprefs.get('editor_%s_toolbar' % self.syntax, ()):
            add_action(name, self.tools_bar)

        if self.syntax == 'html':
            self.format_bar.clear()
            for name in tprefs['editor_format_toolbar']:
                add_action(name, self.format_bar)
        self.restore_state()

    def break_cycles(self):
        for x in ('modification_state_changed', 'word_ignored', 'link_clicked', 'smart_highlighting_updated'):
            try:
                getattr(self, x).disconnect()
            except TypeError:
                pass  # in case this signal was never connected
        self.undo_redo_state_changed.disconnect()
        self.copy_available_state_changed.disconnect()
        self.cursor_position_changed.disconnect()
        self.data_changed.disconnect()
        self.editor.undoAvailable.disconnect()
        self.editor.redoAvailable.disconnect()
        self.editor.modificationChanged.disconnect()
        self.editor.textChanged.disconnect()
        self.editor.copyAvailable.disconnect()
        self.editor.cursorPositionChanged.disconnect()
        self.editor.link_clicked.disconnect()
        self.editor.smart_highlighting_updated.disconnect()
        self.editor.setPlainText('')
        self.editor.smarts = None
        self.editor.request_completion = None

    def _modification_state_changed(self):
        self.is_synced_to_container = self.is_modified
        self.modification_state_changed.emit(self.is_modified)

    def _data_changed(self):
        self.is_synced_to_container = False
        self.data_changed.emit(self)

    def _undo_available(self, available):
        self.undo_available = available
        self.undo_redo_state_changed.emit(self.undo_available, self.redo_available)

    def _redo_available(self, available):
        self.redo_available = available
        self.undo_redo_state_changed.emit(self.undo_available, self.redo_available)

    def _copy_available(self, available):
        self.copy_available = self.cut_available = available
        self.copy_available_state_changed.emit(available)

    def _cursor_position_changed(self, *args):
        self.cursor_position_changed.emit()

    @property
    def cursor_position(self):
        c = self.editor.textCursor()
        char = ''
        col = c.positionInBlock()
        if not c.atStart():
            c.clearSelection()
            c.movePosition(c.PreviousCharacter, c.KeepAnchor)
            char = unicode_type(c.selectedText()).rstrip('\0')
        return (c.blockNumber() + 1, col, char)

    def cut(self):
        self.editor.cut()

    def copy(self):
        self.editor.copy()

    def go_to_line(self, line, col=None):
        self.editor.go_to_line(line, col=col)

    def paste(self):
        if not self.editor.canPaste():
            return error_dialog(self, _('No text'), _(
                'There is no suitable text in the clipboard to paste.'), show=True)
        self.editor.paste()

    def contextMenuEvent(self, ev):
        ev.ignore()

    def fix_html(self):
        if self.syntax == 'html':
            from calibre.ebooks.oeb.polish.pretty import fix_html
            self.editor.replace_text(fix_html(current_container(), unicode_type(self.editor.toPlainText())).decode('utf-8'))
            return True
        return False

    def pretty_print(self, name):
        from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml
        if self.syntax in {'css', 'html', 'xml'}:
            func = {'css':pretty_css, 'xml':pretty_xml}.get(self.syntax, pretty_html)
            original_text = unicode_type(self.editor.toPlainText())
            prettied_text = func(current_container(), name, original_text).decode('utf-8')
            if original_text != prettied_text:
                self.editor.replace_text(prettied_text)
            return True
        return False

    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction
        c = self.editor.cursorForPosition(pos)
        origc = QTextCursor(c)
        current_cursor = self.editor.textCursor()
        r = origr = self.editor.syntax_range_for_cursor(c)
        if (r is None or not r.format.property(SPELL_PROPERTY)) and c.positionInBlock() > 0 and not current_cursor.hasSelection():
            c.setPosition(c.position() - 1)
            r = self.editor.syntax_range_for_cursor(c)

        if r is not None and r.format.property(SPELL_PROPERTY):
            word = self.editor.text_for_range(c.block(), r)
            locale = self.editor.spellcheck_locale_for_cursor(c)
            orig_pos = c.position()
            c.setPosition(orig_pos - utf16_length(word))
            found = False
            self.editor.setTextCursor(c)
            if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False):
                found = True
                fc = self.editor.textCursor()
                if fc.position() < c.position():
                    self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False)
            spell_cursor = self.editor.textCursor()
            if current_cursor.hasSelection():
                # Restore the current cursor so that any selection is preserved
                # for the change case actions
                self.editor.setTextCursor(current_cursor)
            if found:
                suggestions = dictionaries.suggestions(word, locale)[:7]
                if suggestions:
                    for suggestion in suggestions:
                        ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion, cursor=spell_cursor))
                        f = ac.font()
                        f.setBold(True), ac.setFont(f)
                    m.addSeparator()
                m.addAction(actions['spell-next'])
                m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale))
                dics = dictionaries.active_user_dictionaries
                if len(dics) > 0:
                    if len(dics) == 1:
                        m.addAction(_('Add this word to the dictionary: {0}').format(dics[0].name), partial(
                            self._nuke_word, dics[0].name, word, locale))
                    else:
                        ac = m.addAction(_('Add this word to the dictionary'))
                        dmenu = QMenu(m)
                        ac.setMenu(dmenu)
                        for dic in dics:
                            dmenu.addAction(dic.name, partial(self._nuke_word, dic.name, word, locale))
                m.addSeparator()

        if origr is not None and origr.format.property(LINK_PROPERTY):
            href = self.editor.text_for_range(origc.block(), origr)
            m.addAction(_('Open %s') % href, partial(self.link_clicked.emit, href))

        if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)):
            word = self.editor.text_for_range(origc.block(), origr)
            item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY) else 'css_property'
            url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version)
            if url is not None:
                m.addAction(_('Show help for: %s') % word, partial(open_url, url))

        for x in ('undo', 'redo'):
            ac = actions['editor-%s' % x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        for x in ('cut', 'copy', 'paste'):
            ac = actions['editor-' + x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        m.addAction(_('&Select all'), self.editor.select_all)
        if self.selected_text or self.has_marked_text:
            update_mark_text_action(self)
            m.addAction(actions['mark-selected-text'])
        if self.syntax != 'css' and actions['editor-cut'].isEnabled():
            cm = QMenu(_('Change &case'), m)
            for ac in 'upper lower swap title capitalize'.split():
                cm.addAction(actions['transform-case-' + ac])
            m.addMenu(cm)
        if self.syntax == 'html':
            m.addAction(actions['multisplit'])
        m.exec_(self.editor.viewport().mapToGlobal(pos))

    def goto_sourceline(self, *args, **kwargs):
        return self.editor.goto_sourceline(*args, **kwargs)

    def goto_css_rule(self, *args, **kwargs):
        return self.editor.goto_css_rule(*args, **kwargs)

    def get_tag_contents(self, *args, **kwargs):
        return self.editor.get_tag_contents(*args, **kwargs)

    def _nuke_word(self, dic, word, locale):
        if dic is None:
            dictionaries.ignore_word(word, locale)
        else:
            dictionaries.add_to_user_dictionary(dic, word, locale)
        self.word_ignored.emit(word, locale)
示例#31
0
class ItemDelegate(QStyledItemDelegate):  # {{{

    rename_requested = pyqtSignal(object, object)

    def setEditorData(self, editor, index):
        name = unicode_type(index.data(NAME_ROLE) or '')
        # We do this because Qt calls selectAll() unconditionally on the
        # editor, and we want only a part of the file name to be selected
        QTimer.singleShot(0, partial(self.set_editor_data, name, editor))

    def set_editor_data(self, name, editor):
        if sip.isdeleted(editor):
            return
        editor.setText(name)
        ext_pos = name.rfind('.')
        slash_pos = name.rfind('/')
        if slash_pos == -1 and ext_pos > 0:
            editor.setSelection(0, ext_pos)
        elif ext_pos > -1 and slash_pos > -1 and ext_pos > slash_pos + 1:
            editor.setSelection(slash_pos + 1, ext_pos - slash_pos - 1)
        else:
            editor.selectAll()

    def setModelData(self, editor, model, index):
        newname = unicode_type(editor.text())
        oldname = unicode_type(index.data(NAME_ROLE) or '')
        if newname != oldname:
            self.rename_requested.emit(oldname, newname)

    def sizeHint(self, option, index):
        ans = QStyledItemDelegate.sizeHint(self, option, index)
        top_level = not index.parent().isValid()
        ans += QSize(0, 20 if top_level else 10)
        return ans

    def paint(self, painter, option, index):
        top_level = not index.parent().isValid()
        hover = option.state & QStyle.State_MouseOver
        if hover:
            if top_level:
                suffix = '%s(%d)' % (NBSP, index.model().rowCount(index))
            else:
                try:
                    suffix = NBSP + human_readable(
                        current_container().filesize(
                            unicode_type(index.data(NAME_ROLE) or '')))
                except EnvironmentError:
                    suffix = NBSP + human_readable(0)
            br = painter.boundingRect(option.rect,
                                      Qt.AlignRight | Qt.AlignVCenter, suffix)
        if top_level and index.row() > 0:
            option.rect.adjust(0, 5, 0, 0)
            painter.drawLine(option.rect.topLeft(), option.rect.topRight())
            option.rect.adjust(0, 1, 0, 0)
        if hover:
            option.rect.adjust(0, 0, -br.width(), 0)
        QStyledItemDelegate.paint(self, painter, option, index)
        if hover:
            option.rect.adjust(0, 0, br.width(), 0)
            painter.drawText(option.rect, Qt.AlignRight | Qt.AlignVCenter,
                             suffix)