예제 #1
0
class Comments(HTMLDisplay):  # {{{

    def __init__(self, parent=None):
        HTMLDisplay.__init__(self, parent)
        self.setAcceptDrops(False)
        self.wait_timer = QTimer(self)
        self.wait_timer.timeout.connect(self.update_wait)
        self.wait_timer.setInterval(800)
        self.dots_count = 0
        self.anchor_clicked.connect(self.link_activated)

    def link_activated(self, url):
        from calibre.gui2 import open_url
        if url.scheme() in {'http', 'https'}:
            open_url(url)

    def show_wait(self):
        self.dots_count = 0
        self.wait_timer.start()
        self.update_wait()

    def update_wait(self):
        self.dots_count += 1
        self.dots_count %= 10
        self.dots_count = self.dots_count or 1
        self.setHtml(
            '<h2>'+_('Please wait')+
            '<br><span id="dots">{}</span></h2>'.format('.' * self.dots_count))

    def show_data(self, html):
        self.wait_timer.stop()

        def color_to_string(col):
            ans = '#000000'
            if col.isValid():
                col = col.toRgb()
                if col.isValid():
                    ans = str(col.name())
            return ans

        c = color_to_string(QApplication.palette().color(QPalette.ColorGroup.Normal,
                        QPalette.ColorRole.WindowText))
        templ = '''\
        <html>
            <head>
            <style type="text/css">
                body, td {background-color: transparent; color: %s }
                a { text-decoration: none; }
                div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
                table { margin-bottom: 0; padding-bottom: 0; }
            </style>
            </head>
            <body>
            <div class="description">
            %%s
            </div>
            </body>
        <html>
        '''%(c,)
        self.setHtml(templ%html)
예제 #2
0
class DetailView(Dialog):  # {{{
    def __init__(self, parent, job):
        self.job = job
        self.html_view = hasattr(job, 'html_details') and not getattr(
            job, 'ignore_html_details', False)
        Dialog.__init__(self, job.description, 'job-detail-view-dialog',
                        parent)

    def sizeHint(self):
        return QSize(700, 500)

    @property
    def plain_text(self):
        if self.html_view:
            return self.tb.toPlainText()
        return self.log.toPlainText()

    def copy_to_clipboard(self):
        QApplication.instance().clipboard().setText(self.plain_text)

    def setup_ui(self):
        self.l = l = QVBoxLayout(self)
        if self.html_view:
            self.tb = w = QTextBrowser(self)
        else:
            self.log = w = QPlainTextEdit(self)
            w.setReadOnly(True), w.setLineWrapMode(
                QPlainTextEdit.LineWrapMode.NoWrap)
        l.addWidget(w)
        l.addWidget(self.bb)
        self.bb.clear(), self.bb.setStandardButtons(
            QDialogButtonBox.StandardButton.Close)
        self.copy_button = b = self.bb.addButton(
            _('&Copy to clipboard'), QDialogButtonBox.ButtonRole.ActionRole)
        b.setIcon(QIcon(I('edit-copy.png')))
        b.clicked.connect(self.copy_to_clipboard)
        self.next_pos = 0
        self.update()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(1000)
        if not self.html_view:
            v = self.log.verticalScrollBar()
            v.setValue(v.maximum())

    def update(self):
        if self.html_view:
            html = self.job.html_details
            if len(html) > self.next_pos:
                self.next_pos = len(html)
                self.tb.setHtml('<pre style="font-family:monospace">%s</pre>' %
                                html)
        else:
            f = self.job.log_file
            f.seek(self.next_pos)
            more = f.read()
            self.next_pos = f.tell()
            if more:
                self.log.appendPlainText(more.decode('utf-8', 'replace'))
예제 #3
0
class GarbageCollector(QObject):

    '''
    Disable automatic garbage collection and instead collect manually
    every INTERVAL milliseconds.

    This is done to ensure that garbage collection only happens in the GUI
    thread, as otherwise Qt can crash.
    '''

    INTERVAL = 5000

    def __init__(self, parent, debug=False):
        QObject.__init__(self, parent)
        self.debug = debug

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.check)

        self.threshold = gc.get_threshold()
        gc.disable()
        self.timer.start(self.INTERVAL)
        # gc.set_debug(gc.DEBUG_SAVEALL)

    def check(self):
        # return self.debug_cycles()
        l0, l1, l2 = gc.get_count()
        if self.debug:
            print('gc_check called:', l0, l1, l2)
        if l0 > self.threshold[0]:
            num = gc.collect(0)
            if self.debug:
                print('collecting gen 0, found:', num, 'unreachable')
            if l1 > self.threshold[1]:
                num = gc.collect(1)
                if self.debug:
                    print('collecting gen 1, found:', num, 'unreachable')
                if l2 > self.threshold[2]:
                    num = gc.collect(2)
                    if self.debug:
                        print('collecting gen 2, found:', num, 'unreachable')

    def debug_cycles(self):
        gc.collect()
        for obj in gc.garbage:
            print(obj, repr(obj), type(obj))
예제 #4
0
파일: ui.py 프로젝트: smdx023/calibre
class TagBrowserWidget(QFrame):  # {{{

    def __init__(self, parent):
        QFrame.__init__(self, parent)
        self.setFrameStyle(QFrame.Shape.NoFrame if gprefs['tag_browser_old_look'] else QFrame.Shape.StyledPanel)
        self._parent = parent
        self._layout = QVBoxLayout(self)
        self._layout.setContentsMargins(0,0,0,0)

        # Set up the find box & button
        self.tb_bar = tbb = TagBrowserBar(self)
        tbb.clear_find.connect(self.reset_find)
        self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button
        self.toggle_search_button = tbb.toggle_search_button
        self._layout.addWidget(tbb)

        self.current_find_position = None
        self.search_button.clicked.connect(self.find)
        self.item_search.lineEdit().textEdited.connect(self.find_text_changed)
        self.item_search.activated[str].connect(self.do_find)

        # The tags view
        parent.tags_view = TagsView(parent)
        self.tags_view = parent.tags_view
        self._layout.insertWidget(0, parent.tags_view)

        # Now the floating 'not found' box
        l = QLabel(self.tags_view)
        self.not_found_label = l
        l.setFrameStyle(QFrame.Shape.StyledPanel)
        l.setAutoFillBackground(True)
        l.setText('<p><b>'+_('No more matches.</b><p> Click Find again to go to first match'))
        l.setAlignment(Qt.AlignmentFlag.AlignVCenter)
        l.setWordWrap(True)
        l.resize(l.sizeHint())
        l.move(10,20)
        l.setVisible(False)
        self.not_found_label_timer = QTimer()
        self.not_found_label_timer.setSingleShot(True)
        self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event,
                                                   type=Qt.ConnectionType.QueuedConnection)
        self.collapse_all_action = ac = QAction(parent)
        parent.addAction(ac)
        parent.keyboard.register_shortcut('tag browser collapse all',
                _('Collapse all'), default_keys=(),
                action=ac, group=_('Tag browser'))
        connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll())

        # The Configure Tag Browser button
        l = self.alter_tb
        ac = QAction(parent)
        parent.addAction(ac)
        parent.keyboard.register_shortcut('tag browser alter',
                _('Configure Tag browser'), default_keys=(),
                action=ac, group=_('Tag browser'))
        ac.triggered.connect(l.showMenu)

        l.m.aboutToShow.connect(self.about_to_show_configure_menu)
        l.m.show_counts_action = ac = l.m.addAction('counts')
        ac.triggered.connect(self.toggle_counts)
        l.m.show_avg_rating_action = ac = l.m.addAction('avg rating')
        ac.triggered.connect(self.toggle_avg_rating)
        sb = l.m.addAction(_('Sort by'))
        sb.m = l.sort_menu = QMenu(l.m)
        sb.setMenu(sb.m)
        sb.bg = QActionGroup(sb)

        # Must be in the same order as db2.CATEGORY_SORTS
        for i, x in enumerate((_('Name'), _('Number of books'),
                  _('Average rating'))):
            a = sb.m.addAction(x)
            sb.bg.addAction(a)
            a.setCheckable(True)
            if i == 0:
                a.setChecked(True)
        sb.setToolTip(
                _('Set the sort order for entries in the Tag browser'))
        sb.setStatusTip(sb.toolTip())

        ma = l.m.addAction(_('Search type when selecting multiple items'))
        ma.m = l.match_menu = QMenu(l.m)
        ma.setMenu(ma.m)
        ma.ag = QActionGroup(ma)

        # Must be in the same order as db2.MATCH_TYPE
        for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))):
            a = ma.m.addAction(x)
            ma.ag.addAction(a)
            a.setCheckable(True)
            if i == 0:
                a.setChecked(True)
        ma.setToolTip(
                _('When selecting multiple entries in the Tag browser '
                    'match any or all of them'))
        ma.setStatusTip(ma.toolTip())

        mt = l.m.addAction(_('Manage authors, tags, etc.'))
        mt.setToolTip(_('All of these category_managers are available by right-clicking '
                       'on items in the Tag browser above'))
        mt.m = l.manage_menu = QMenu(l.m)
        mt.setMenu(mt.m)

        ac = QAction(parent)
        parent.addAction(ac)
        parent.keyboard.register_shortcut('tag browser toggle item',
                _("'Click' found item"), default_keys=(),
                action=ac, group=_('Tag browser'))
        ac.triggered.connect(self.toggle_item)

        ac = QAction(parent)
        parent.addAction(ac)
        parent.keyboard.register_shortcut('tag browser set focus',
                _("Give the Tag browser keyboard focus"), default_keys=(),
                action=ac, group=_('Tag browser'))
        ac.triggered.connect(self.give_tb_focus)

        # self.leak_test_timer = QTimer(self)
        # self.leak_test_timer.timeout.connect(self.test_for_leak)
        # self.leak_test_timer.start(5000)

    def about_to_show_configure_menu(self):
        ac = self.alter_tb.m.show_counts_action
        ac.setText(_('Hide counts') if gprefs['tag_browser_show_counts'] else _('Show counts'))
        ac = self.alter_tb.m.show_avg_rating_action
        ac.setText(_('Hide average rating') if config['show_avg_rating'] else _('Show average rating'))

    def toggle_counts(self):
        gprefs['tag_browser_show_counts'] ^= True

    def toggle_avg_rating(self):
        config['show_avg_rating'] ^= True

    def save_state(self):
        gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked())

    def toggle_item(self):
        self.tags_view.toggle_current_index()

    def give_tb_focus(self, *args):
        if gprefs['tag_browser_allow_keyboard_focus']:
            tb = self.tags_view
            if tb.hasFocus():
                self._parent.shift_esc()
            elif self._parent.current_view() == self._parent.library_view:
                tb.setFocus()
                idx = tb.currentIndex()
                if not idx.isValid():
                    idx = tb.model().createIndex(0, 0)
                    tb.setCurrentIndex(idx)

    def set_pane_is_visible(self, to_what):
        self.tags_view.set_pane_is_visible(to_what)
        if not to_what:
            self._parent.shift_esc()

    def find_text_changed(self, str_):
        self.current_find_position = None

    def set_focus_to_find_box(self):
        self.tb_bar.set_focus_to_find_box()

    def do_find(self, str_=None):
        self.current_find_position = None
        self.find()

    @property
    def find_text(self):
        return str(self.item_search.currentText()).strip()

    def reset_find(self):
        model = self.tags_view.model()
        model.clear_boxed()
        if model.get_categories_filter():
            model.set_categories_filter(None)
            self.tags_view.recount()
            self.current_find_position = None

    def find(self):
        model = self.tags_view.model()
        model.clear_boxed()

        # When a key is specified don't use the auto-collapsing search.
        # A colon separates the lookup key from the search string.
        # A leading colon says not to use autocollapsing search but search all keys
        txt = self.find_text
        colon = txt.find(':')
        if colon >= 0:
            key = self._parent.library_view.model().db.\
                        field_metadata.search_term_to_field_key(txt[:colon])
            if key in self._parent.library_view.model().db.field_metadata:
                txt = txt[colon+1:]
            else:
                key = ''
                txt = txt[1:] if colon == 0 else txt
        else:
            key = None

        # key is None indicates that no colon was found.
        # key == '' means either a leading : was found or the key is invalid

        # At this point the txt might have a leading =, in which case do an
        # exact match search

        if (gprefs.get('tag_browser_always_autocollapse', False) and
                key is None and not txt.startswith('*')):
            txt = '*' + txt
        if txt.startswith('*'):
            self.tags_view.collapseAll()
            model.set_categories_filter(txt[1:])
            self.tags_view.recount()
            self.current_find_position = None
            return
        if model.get_categories_filter():
            model.set_categories_filter(None)
            self.tags_view.recount()
            self.current_find_position = None

        if not txt:
            return

        self.item_search.lineEdit().blockSignals(True)
        self.search_button.setFocus(True)
        self.item_search.lineEdit().blockSignals(False)

        if txt.startswith('='):
            equals_match = True
            txt = txt[1:]
        else:
            equals_match = False
        self.current_find_position = \
            model.find_item_node(key, txt, self.current_find_position,
                                 equals_match=equals_match)

        if self.current_find_position:
            self.tags_view.show_item_at_path(self.current_find_position, box=True)
        elif self.item_search.text():
            self.not_found_label.setVisible(True)
            if self.tags_view.verticalScrollBar().isVisible():
                sbw = self.tags_view.verticalScrollBar().width()
            else:
                sbw = 0
            width = self.width() - 8 - sbw
            height = self.not_found_label.heightForWidth(width) + 20
            self.not_found_label.resize(width, height)
            self.not_found_label.move(4, 10)
            self.not_found_label_timer.start(2000)

    def not_found_label_timer_event(self):
        self.not_found_label.setVisible(False)

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and self.item_search.hasFocus():
            self.find()
            ev.accept()
            return
        return QFrame.keyPressEvent(self, ev)
예제 #5
0
파일: widgets.py 프로젝트: cbhaley/calibre
class Splitter(QSplitter):

    state_changed = pyqtSignal(object)
    reapply_sizes = pyqtSignal(object)

    def __init__(self,
                 name,
                 label,
                 icon,
                 initial_show=True,
                 initial_side_size=120,
                 connect_button=True,
                 orientation=Qt.Orientation.Horizontal,
                 side_index=0,
                 parent=None,
                 shortcut=None,
                 hide_handle_on_single_panel=True):
        QSplitter.__init__(self, parent)
        self.reapply_sizes.connect(self.setSizes,
                                   type=Qt.ConnectionType.QueuedConnection)
        self.hide_handle_on_single_panel = hide_handle_on_single_panel
        if hide_handle_on_single_panel:
            self.state_changed.connect(self.update_handle_width)
        self.original_handle_width = self.handleWidth()
        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.desired_side_size = initial_side_size
        self.desired_show = initial_show
        self.resize_timer.setInterval(5)
        self.resize_timer.timeout.connect(self.do_resize)
        self.setOrientation(orientation)
        self.side_index = side_index
        self._name = name
        self.label = label
        self.initial_side_size = initial_side_size
        self.initial_show = initial_show
        self.splitterMoved.connect(self.splitter_moved,
                                   type=Qt.ConnectionType.QueuedConnection)
        self.button = LayoutButton(icon, label, self, shortcut=shortcut)
        if connect_button:
            self.button.clicked.connect(self.double_clicked)

        if shortcut is not None:
            self.action_toggle = QAction(QIcon(icon),
                                         _('Toggle') + ' ' + label, self)
            self.action_toggle.changed.connect(self.update_shortcut)
            self.action_toggle.triggered.connect(self.toggle_triggered)
            if parent is not None:
                parent.addAction(self.action_toggle)
                if hasattr(parent, 'keyboard'):
                    parent.keyboard.register_shortcut(
                        'splitter %s %s' % (name, label),
                        str(self.action_toggle.text()),
                        default_keys=(shortcut, ),
                        action=self.action_toggle)
                else:
                    self.action_toggle.setShortcut(shortcut)
            else:
                self.action_toggle.setShortcut(shortcut)

    def update_shortcut(self):
        self.button.update_shortcut(self.action_toggle)

    def toggle_triggered(self, *args):
        self.toggle_side_pane()

    def createHandle(self):
        return SplitterHandle(self.orientation(), self)

    def initialize(self):
        for i in range(self.count()):
            h = self.handle(i)
            if h is not None:
                h.splitter_moved()
        self.state_changed.emit(not self.is_side_index_hidden)

    def splitter_moved(self, *args):
        self.desired_side_size = self.side_index_size
        self.state_changed.emit(not self.is_side_index_hidden)

    def update_handle_width(self, not_one_panel):
        self.setHandleWidth(self.original_handle_width if not_one_panel else 0)

    @property
    def is_side_index_hidden(self):
        sizes = list(self.sizes())
        try:
            return sizes[self.side_index] == 0
        except IndexError:
            return True

    @property
    def save_name(self):
        ori = 'horizontal' if self.orientation() == Qt.Orientation.Horizontal \
                else 'vertical'
        return self._name + '_' + ori

    def print_sizes(self):
        if self.count() > 1:
            print(self.save_name,
                  'side:',
                  self.side_index_size,
                  'other:',
                  end=' ')
            print(list(self.sizes())[self.other_index])

    @property
    def side_index_size(self):
        if self.count() < 2:
            return 0
        return self.sizes()[self.side_index]

    @side_index_size.setter
    def side_index_size(self, val):
        if self.count() < 2:
            return
        side_index_hidden = self.is_side_index_hidden
        if val == 0 and not side_index_hidden:
            self.save_state()
        sizes = list(self.sizes())
        for i in range(len(sizes)):
            sizes[i] = val if i == self.side_index else 10
        self.setSizes(sizes)
        sizes = list(self.sizes())
        total = sum(sizes)
        total_needs_adjustment = self.hide_handle_on_single_panel and side_index_hidden
        if total_needs_adjustment:
            total -= self.original_handle_width
        for i in range(len(sizes)):
            sizes[i] = val if i == self.side_index else total - val
        self.setSizes(sizes)
        self.initialize()
        if total_needs_adjustment:
            # the handle visibility and therefore size distribution will change
            # when the event loop ticks
            self.reapply_sizes.emit(sizes)

    def do_resize(self, *args):
        orig = self.desired_side_size
        QSplitter.resizeEvent(self, self._resize_ev)
        if orig > 20 and self.desired_show:
            c = 0
            while abs(self.side_index_size - orig) > 10 and c < 5:
                self.apply_state(self.get_state(), save_desired=False)
                c += 1

    def resizeEvent(self, ev):
        if self.resize_timer.isActive():
            self.resize_timer.stop()
        self._resize_ev = ev
        self.resize_timer.start()

    def get_state(self):
        if self.count() < 2:
            return (False, 200)
        return (self.desired_show, self.desired_side_size)

    def apply_state(self, state, save_desired=True):
        if state[0]:
            self.side_index_size = state[1]
            if save_desired:
                self.desired_side_size = self.side_index_size
        else:
            self.side_index_size = 0
        self.desired_show = state[0]

    def default_state(self):
        return (self.initial_show, self.initial_side_size)

    # Public API {{{

    def update_desired_state(self):
        self.desired_show = not self.is_side_index_hidden

    def save_state(self):
        if self.count() > 1:
            gprefs[self.save_name + '_state'] = self.get_state()

    @property
    def other_index(self):
        return (self.side_index + 1) % 2

    def restore_state(self):
        if self.count() > 1:
            state = gprefs.get(self.save_name + '_state', self.default_state())
            self.apply_state(state, save_desired=False)
            self.desired_side_size = state[1]

    def toggle_side_pane(self, hide=None):
        if hide is None:
            action = 'show' if self.is_side_index_hidden else 'hide'
        else:
            action = 'hide' if hide else 'show'
        getattr(self, action + '_side_pane')()

    def show_side_pane(self):
        if self.count() < 2 or not self.is_side_index_hidden:
            return
        if self.desired_side_size == 0:
            self.desired_side_size = self.initial_side_size
        self.apply_state((True, self.desired_side_size))

    def hide_side_pane(self):
        if self.count() < 2 or self.is_side_index_hidden:
            return
        self.apply_state((False, self.desired_side_size))

    def double_clicked(self, *args):
        self.toggle_side_pane()
예제 #6
0
파일: preview.py 프로젝트: smdx023/calibre
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()
    live_css_data = pyqtSignal(object)
    render_process_restarted = pyqtSignal()
    open_file_with = pyqtSignal(object, object, object)
    edit_file = pyqtSignal(object)

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.l = l = QVBoxLayout()
        self.setLayout(l)
        l.setContentsMargins(0, 0, 0, 0)
        self.stack = QStackedLayout(l)
        self.stack.setStackingMode(QStackedLayout.StackingMode.StackAll)
        self.current_sync_retry_count = 0
        self.view = WebView(self)
        self.view._page.bridge.request_sync.connect(self.request_sync)
        self.view._page.bridge.request_split.connect(self.request_split)
        self.view._page.bridge.live_css_data.connect(self.live_css_data)
        self.view._page.bridge.bridge_ready.connect(self.on_bridge_ready)
        self.view._page.loadFinished.connect(self.load_finished)
        self.view._page.loadStarted.connect(self.load_started)
        self.view.render_process_restarted.connect(self.render_process_restarted)
        self.pending_go_to_anchor = None
        self.inspector = self.view.inspector
        self.stack.addWidget(self.view)
        self.cover = c = QLabel(_('Loading preview, please wait...'))
        c.setWordWrap(True)
        c.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        c.setStyleSheet('QLabel { background-color: palette(window); }')
        c.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.stack.addWidget(self.cover)
        self.stack.setCurrentIndex(self.stack.indexOf(self.cover))
        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.setClearButtonEnabled(True)
        ac = self.search.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
        if ac is not None:
            ac.triggered.connect(self.clear_clicked)
        self.search.initialize('tweak_book_preview_search')
        self.search.setPlaceholderText(_('Search in preview'))
        self.search.returnPressed.connect(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(getattr(self, 'find_' + d))
            self.bar.addAction(ac)

    def clear_clicked(self):
        self.view._page.findText('')

    def find(self, direction):
        text = str(self.search.text())
        self.view._page.findText(text, (
            QWebEnginePage.FindFlag.FindBackward if direction == 'prev' else QWebEnginePage.FindFlags(0)))

    def find_next(self):
        self.find('next')

    def find_prev(self):
        self.find('prev')

    def go_to_anchor(self, anchor):
        self.view._page.go_to_anchor(anchor)

    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.go_to_anchor(urlparse(href).fragment)
                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):
        actions['split-in-preview'].setChecked(False)
        if not loc or not totals:
            return error_dialog(self, _('Invalid location'),
                                _('Cannot split on the body tag'), show=True)
        if self.current_name:
            self.split_requested.emit(self.current_name, loc, totals)

    @property
    def bridge_ready(self):
        return self.view._page.bridge.ready

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

    def _sync_to_editor(self):
        if not actions['sync-preview-to-editor'].isChecked() or self.current_sync_retry_count >= 3000 or self.current_sync_request is None:
            return
        if self.refresh_timer.isActive() or not self.bridge_ready or self.current_sync_request[0] != self.current_name:
            self.current_sync_retry_count += 1
            return QTimer.singleShot(100, self._sync_to_editor)
        sourceline_address = self.current_sync_request[1]
        self.current_sync_request = None
        self.current_sync_retry_count = 0
        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 name_to_qurl(self, name=None):
        name = name or self.current_name
        qurl = QUrl()
        qurl.setScheme(FAKE_PROTOCOL), qurl.setAuthority(FAKE_HOST), qurl.setPath('/' + name)
        return qurl

    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.set_url(self.name_to_qurl())
            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 = self.name_to_qurl()
            self.refresh_starting.emit()
            if current_url != self.view.url():
                # The container was changed
                self.view.set_url(current_url)
            else:
                self.view.refresh()
            self.refreshed.emit()

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

    @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('<p>' + (_(
            'Abort file split') if checked else _(
                'Split this file at a specified location.<p>After 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_started(self):
        self.stack.setCurrentIndex(self.stack.indexOf(self.cover))

    def on_bridge_ready(self):
        self.stack.setCurrentIndex(self.stack.indexOf(self.view))

    def load_finished(self, ok):
        self.stack.setCurrentIndex(self.stack.indexOf(self.view))
        if self.pending_go_to_anchor:
            self.view._page.go_to_anchor(self.pending_go_to_anchor)
            self.pending_go_to_anchor = None
        if actions['split-in-preview'].isChecked():
            if ok:
                self.do_start_split()
            else:
                self.stop_split()

    def request_live_css_data(self, editor_name, sourceline, tags):
        if self.view._page.bridge.ready:
            self.view._page.bridge.live_css(editor_name, sourceline, tags)

    def apply_settings(self):
        s = self.view.settings()
        s.setFontSize(QWebEngineSettings.FontSize.DefaultFontSize, tprefs['preview_base_font_size'])
        s.setFontSize(QWebEngineSettings.FontSize.DefaultFixedFontSize, tprefs['preview_mono_font_size'])
        s.setFontSize(QWebEngineSettings.FontSize.MinimumLogicalFontSize, tprefs['preview_minimum_font_size'])
        s.setFontSize(QWebEngineSettings.FontSize.MinimumFontSize, tprefs['preview_minimum_font_size'])
        sf, ssf, mf = tprefs['engine_preview_serif_family'], tprefs['engine_preview_sans_family'], tprefs['engine_preview_mono_family']
        if sf:
            s.setFontFamily(QWebEngineSettings.FontFamily.SerifFont, sf)
        if ssf:
            s.setFontFamily(QWebEngineSettings.FontFamily.SansSerifFont, ssf)
        if mf:
            s.setFontFamily(QWebEngineSettings.FontFamily.FixedFont, mf)
        stdfnt = tprefs['preview_standard_font_family'] or 'serif'
        stdfnt = {
            'serif': QWebEngineSettings.FontFamily.SerifFont,
            'sans': QWebEngineSettings.FontFamily.SansSerifFont,
            'mono': QWebEngineSettings.FontFamily.FixedFont
        }[stdfnt]
        s.setFontFamily(QWebEngineSettings.FontFamily.StandardFont, s.fontFamily(stdfnt))
예제 #7
0
class SearchBox2(QComboBox):  # {{{

    '''
    To use this class:

        * Call initialize()
        * Connect to the search() and cleared() signals from this widget.
        * Connect to the changed() signal to know when the box content changes
        * Connect to focus_to_library() signal to be told to manually change focus
        * Call search_done() after every search is complete
        * Call set_search_string() to perform a search programmatically
        * You can use the current_text property to get the current search text
          Be aware that if you are using it in a slot connected to the
          changed() signal, if the connection is not queued it will not be
          accurate.
    '''

    INTERVAL = 1500  #: Time to wait before emitting search signal
    MAX_COUNT = 25

    search  = pyqtSignal(object)
    cleared = pyqtSignal()
    changed = pyqtSignal()
    focus_to_library = pyqtSignal()

    def __init__(self, parent=None, add_clear_action=True, as_url=None):
        QComboBox.__init__(self, parent)
        self.line_edit = SearchLineEdit(self)
        self.line_edit.as_url = as_url
        self.setLineEdit(self.line_edit)
        self.line_edit.clear_history.connect(self.clear_history)
        if add_clear_action:
            self.lineEdit().setClearButtonEnabled(True)
            ac = self.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
            if ac is not None:
                ac.triggered.connect(self.clear_clicked)

        c = self.line_edit.completer()
        c.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
        c.highlighted[native_string_type].connect(self.completer_used)

        self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.ConnectionType.DirectConnection)
        # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807
        self.activated[native_string_type].connect(self.history_selected, type=Qt.ConnectionType.QueuedConnection)
        self.setEditable(True)
        self.as_you_type = True
        self.timer = QTimer()
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.timer_event, type=Qt.ConnectionType.QueuedConnection)
        self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
        self.setMaxCount(self.MAX_COUNT)
        self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
        self.setMinimumContentsLength(25)
        self._in_a_search = False
        self.tool_tip_text = self.toolTip()

    def add_action(self, icon, position=QLineEdit.ActionPosition.TrailingPosition):
        if not isinstance(icon, QIcon):
            icon = QIcon(I(icon))
        return self.lineEdit().addAction(icon, position)

    def initialize(self, opt_name, colorize=False, help_text=_('Search'), as_you_type=None):
        self.as_you_type = config['search_as_you_type'] if as_you_type is None else as_you_type
        self.opt_name = opt_name
        items = []
        for item in config[opt_name]:
            if item not in items:
                items.append(item)
        self.addItems(items)
        self.line_edit.setPlaceholderText(help_text)
        self.colorize = colorize
        self.clear()

    def clear_history(self):
        config[self.opt_name] = []
        self.clear()
    clear_search_history = clear_history

    def hide_completer_popup(self):
        try:
            self.lineEdit().completer().popup().setVisible(False)
        except:
            pass

    def normalize_state(self):
        self.setToolTip(self.tool_tip_text)
        self.line_edit.setStyleSheet('')

    def text(self):
        return self.currentText()

    def clear(self, emit_search=True):
        self.normalize_state()
        self.setEditText('')
        if emit_search:
            self.search.emit('')
        self._in_a_search = False
        self.cleared.emit()

    def clear_clicked(self, *args):
        self.clear()
        self.setFocus(Qt.FocusReason.OtherFocusReason)

    def search_done(self, ok):
        if isinstance(ok, string_or_bytes):
            self.setToolTip(ok)
            ok = False
        if not unicode_type(self.currentText()).strip():
            self.clear(emit_search=False)
            return
        self._in_a_search = ok
        if self.colorize:
            self.line_edit.setStyleSheet(QApplication.instance().stylesheet_for_line_edit(not ok))
        else:
            self.line_edit.setStyleSheet('')

    # Comes from the lineEdit control
    def key_pressed(self, event):
        k = event.key()
        if k in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
                Qt.Key.Key_Home, Qt.Key.Key_End, Qt.Key.Key_PageUp, Qt.Key.Key_PageDown,
                Qt.Key.Key_unknown):
            return
        self.normalize_state()
        if self._in_a_search:
            self.changed.emit()
            self._in_a_search = False
        if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
            self.do_search()
            self.focus_to_library.emit()
        elif self.as_you_type and unicode_type(event.text()):
            self.timer.start(1500)

    # Comes from the combobox itself
    def keyPressEvent(self, event):
        k = event.key()
        if k in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
            return self.do_search()
        if k not in (Qt.Key.Key_Up, Qt.Key.Key_Down):
            return QComboBox.keyPressEvent(self, event)
        self.blockSignals(True)
        self.normalize_state()
        if k == Qt.Key.Key_Down and self.currentIndex() == 0 and not self.lineEdit().text():
            self.setCurrentIndex(1), self.setCurrentIndex(0)
            event.accept()
        else:
            QComboBox.keyPressEvent(self, event)
        self.blockSignals(False)

    def completer_used(self, text):
        self.timer.stop()
        self.normalize_state()

    def timer_event(self):
        self._do_search(as_you_type=True)

    def history_selected(self, text):
        self.changed.emit()
        self.do_search()

    def _do_search(self, store_in_history=True, as_you_type=False):
        self.hide_completer_popup()
        text = unicode_type(self.currentText()).strip()
        if not text:
            return self.clear()
        if as_you_type:
            text = AsYouType(text)
        self.search.emit(text)

        if store_in_history:
            idx = self.findText(text, Qt.MatchFlag.MatchFixedString|Qt.MatchFlag.MatchCaseSensitive)
            self.block_signals(True)
            if idx < 0:
                self.insertItem(0, text)
            else:
                t = self.itemText(idx)
                self.removeItem(idx)
                self.insertItem(0, t)
            self.setCurrentIndex(0)
            self.block_signals(False)
            history = [unicode_type(self.itemText(i)) for i in
                    range(self.count())]
            config[self.opt_name] = history

    def do_search(self, *args):
        self._do_search()

    def block_signals(self, yes):
        self.blockSignals(yes)
        self.line_edit.blockSignals(yes)

    def set_search_string(self, txt, store_in_history=False, emit_changed=True):
        if not store_in_history:
            self.activated[native_string_type].disconnect()
        try:
            self.setFocus(Qt.FocusReason.OtherFocusReason)
            if not txt:
                self.clear()
            else:
                self.normalize_state()
                # must turn on case sensitivity here so that tag browser strings
                # are not case-insensitively replaced from history
                self.line_edit.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
                self.setEditText(txt)
                self.line_edit.end(False)
                if emit_changed:
                    self.changed.emit()
                self._do_search(store_in_history=store_in_history)
                self.line_edit.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
            self.focus_to_library.emit()
        finally:
            if not store_in_history:
                # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807
                self.activated[native_string_type].connect(self.history_selected, type=Qt.ConnectionType.QueuedConnection)

    def search_as_you_type(self, enabled):
        self.as_you_type = enabled

    def in_a_search(self):
        return self._in_a_search

    @property
    def current_text(self):
        return unicode_type(self.lineEdit().text())
예제 #8
0
class Scheduler(QObject):

    INTERVAL = 1  # minutes

    delete_old_news = pyqtSignal(object)
    start_recipe_fetch = pyqtSignal(object)

    def __init__(self, parent, db):
        QObject.__init__(self, parent)
        self.internet_connection_failed = False
        self._parent = parent
        self.no_internet_msg = _(
            'Cannot download news as no internet connection '
            'is active')
        self.no_internet_dialog = d = error_dialog(self._parent,
                                                   self.no_internet_msg,
                                                   _('No internet connection'),
                                                   show_copy_button=False)
        d.setModal(False)

        self.recipe_model = RecipeModel()
        self.db = db
        self.lock = QMutex(QMutex.RecursionMode.Recursive)
        self.download_queue = set()

        self.news_menu = QMenu()
        self.news_icon = QIcon(I('news.png'))
        self.scheduler_action = QAction(QIcon(I('scheduler.png')),
                                        _('Schedule news download'), self)
        self.news_menu.addAction(self.scheduler_action)
        self.scheduler_action.triggered[bool].connect(self.show_dialog)
        self.cac = QAction(QIcon(I('user_profile.png')),
                           _('Add or edit a custom news source'), self)
        self.cac.triggered[bool].connect(self.customize_feeds)
        self.news_menu.addAction(self.cac)
        self.news_menu.addSeparator()
        self.all_action = self.news_menu.addAction(
            _('Download all scheduled news sources'),
            self.download_all_scheduled)

        self.timer = QTimer(self)
        self.timer.start(int(self.INTERVAL * 60 * 1000))
        self.timer.timeout.connect(self.check)
        self.oldest = gconf['oldest_news']
        QTimer.singleShot(5 * 1000, self.oldest_check)

    def database_changed(self, db):
        self.db = db

    def oldest_check(self):
        if self.oldest > 0:
            delta = timedelta(days=self.oldest)
            try:
                ids = list(
                    self.db.tags_older_than(_('News'),
                                            delta,
                                            must_have_authors=['calibre']))
            except:
                # Happens if library is being switched
                ids = []
            if ids:
                if ids:
                    self.delete_old_news.emit(ids)
        QTimer.singleShot(60 * 60 * 1000, self.oldest_check)

    def show_dialog(self, *args):
        self.lock.lock()
        try:
            d = SchedulerDialog(self.recipe_model)
            d.download.connect(self.download_clicked)
            d.exec_()
            gconf['oldest_news'] = self.oldest = d.old_news.value()
            d.break_cycles()
        finally:
            self.lock.unlock()

    def customize_feeds(self, *args):
        from calibre.gui2.dialogs.custom_recipes import CustomRecipes
        d = CustomRecipes(self.recipe_model, self._parent)
        try:
            d.exec_()
        finally:
            d.deleteLater()

    def do_download(self, urn):
        self.lock.lock()
        try:
            account_info = self.recipe_model.get_account_info(urn)
            customize_info = self.recipe_model.get_customize_info(urn)
            recipe = self.recipe_model.recipe_from_urn(urn)
            un = pw = None
            if account_info is not None:
                un, pw = account_info
            add_title_tag, custom_tags, keep_issues = customize_info
            arg = {
                'username': un,
                'password': pw,
                'add_title_tag': add_title_tag,
                'custom_tags': custom_tags,
                'title': recipe.get('title', ''),
                'urn': urn,
                'keep_issues': keep_issues
            }
            self.download_queue.add(urn)
            self.start_recipe_fetch.emit(arg)
        finally:
            self.lock.unlock()

    def recipe_downloaded(self, arg):
        self.lock.lock()
        try:
            self.recipe_model.update_last_downloaded(arg['urn'])
            self.download_queue.remove(arg['urn'])
        finally:
            self.lock.unlock()

    def recipe_download_failed(self, arg):
        self.lock.lock()
        try:
            self.recipe_model.update_last_downloaded(arg['urn'])
            self.download_queue.remove(arg['urn'])
        finally:
            self.lock.unlock()

    def download_clicked(self, urn):
        if urn is not None:
            return self.download(urn)
        for urn in self.recipe_model.scheduled_urns():
            if not self.download(urn):
                break

    def download_all_scheduled(self):
        self.download_clicked(None)

    def has_internet_connection(self):
        if not internet_connected():
            if not self.internet_connection_failed:
                self.internet_connection_failed = True
                if self._parent.is_minimized_to_tray:
                    self._parent.status_bar.show_message(
                        self.no_internet_msg, 5000)
                elif not self.no_internet_dialog.isVisible():
                    self.no_internet_dialog.show()
            return False
        self.internet_connection_failed = False
        if self.no_internet_dialog.isVisible():
            self.no_internet_dialog.hide()
        return True

    def download(self, urn):
        self.lock.lock()
        if not self.has_internet_connection():
            return False
        doit = urn not in self.download_queue
        self.lock.unlock()
        if doit:
            self.do_download(urn)
        return True

    def check(self):
        recipes = self.recipe_model.get_to_be_downloaded_recipes()
        for urn in recipes:
            if not self.download(urn):
                # No internet connection, we will try again in a minute
                break
예제 #9
0
class TagListEditor(QDialog, Ui_TagListEditor):
    def __init__(self,
                 window,
                 cat_name,
                 tag_to_match,
                 get_book_ids,
                 sorter,
                 ttm_is_first_letter=False,
                 category=None,
                 fm=None):
        QDialog.__init__(self, window)
        Ui_TagListEditor.__init__(self)
        self.setupUi(self)
        self.verticalLayout_2.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.search_box.setMinimumContentsLength(25)

        # Put the category name into the title bar
        t = self.windowTitle()
        self.category_name = cat_name
        self.category = category
        self.setWindowTitle(t + ' (' + cat_name + ')')
        # Remove help icon on title bar
        icon = self.windowIcon()
        self.setWindowFlags(self.windowFlags()
                            & (~Qt.WindowType.WindowContextHelpButtonHint))
        self.setWindowIcon(icon)

        # Get saved geometry info
        try:
            self.table_column_widths = \
                        gprefs.get('tag_list_editor_table_widths', None)
        except:
            pass

        # initialization
        self.to_rename = {}
        self.to_delete = set()
        self.all_tags = {}
        self.original_names = {}

        self.ordered_tags = []
        self.sorter = sorter
        self.get_book_ids = get_book_ids
        self.text_before_editing = ''

        # Capture clicks on the horizontal header to sort the table columns
        hh = self.table.horizontalHeader()
        hh.sectionResized.connect(self.table_column_resized)
        hh.setSectionsClickable(True)
        hh.sectionClicked.connect(self.do_sort)
        hh.setSortIndicatorShown(True)

        self.last_sorted_by = 'name'
        self.name_order = 0
        self.count_order = 1
        self.was_order = 1

        self.edit_delegate = EditColumnDelegate(self.table)
        self.edit_delegate.editing_finished.connect(self.stop_editing)
        self.edit_delegate.editing_started.connect(self.start_editing)
        self.table.setItemDelegateForColumn(0, self.edit_delegate)

        if prefs['case_sensitive']:
            self.string_contains = contains
        else:
            self.string_contains = self.case_insensitive_compare

        self.delete_button.clicked.connect(self.delete_tags)
        self.table.delete_pressed.connect(self.delete_pressed)
        self.rename_button.clicked.connect(self.rename_tag)
        self.undo_button.clicked.connect(self.undo_edit)
        self.table.itemDoubleClicked.connect(self._rename_tag)
        self.table.itemChanged.connect(self.finish_editing)

        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(
            _('&OK'))
        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(
            _('&Cancel'))
        self.buttonBox.accepted.connect(self.accepted)

        self.search_box.initialize('tag_list_search_box_' + cat_name)
        le = self.search_box.lineEdit()
        ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
        if ac is not None:
            ac.triggered.connect(self.clear_search)
        self.search_box.textChanged.connect(self.search_text_changed)
        self.search_button.clicked.connect(self.do_search)
        self.search_button.setDefault(True)
        l = QLabel(self.table)
        self.not_found_label = l
        l.setFrameStyle(QFrame.Shape.StyledPanel)
        l.setAutoFillBackground(True)
        l.setText(_('No matches found'))
        l.setAlignment(Qt.AlignmentFlag.AlignVCenter)
        l.resize(l.sizeHint())
        l.move(10, 0)
        l.setVisible(False)
        self.not_found_label_timer = QTimer()
        self.not_found_label_timer.setSingleShot(True)
        self.not_found_label_timer.timeout.connect(
            self.not_found_label_timer_event,
            type=Qt.ConnectionType.QueuedConnection)

        self.filter_box.initialize('tag_list_filter_box_' + cat_name)
        le = self.filter_box.lineEdit()
        ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
        if ac is not None:
            ac.triggered.connect(self.clear_filter)
        le.returnPressed.connect(self.do_filter)
        self.filter_button.clicked.connect(self.do_filter)

        self.apply_vl_checkbox.clicked.connect(self.vl_box_changed)

        self.table.setEditTriggers(
            QAbstractItemView.EditTrigger.EditKeyPressed)

        try:
            geom = gprefs.get('tag_list_editor_dialog_geometry', None)
            if geom is not None:
                QApplication.instance().safe_restore_geometry(
                    self, QByteArray(geom))
            else:
                self.resize(self.sizeHint() + QSize(150, 100))
        except:
            pass

        self.is_enumerated = False
        if fm:
            if fm['datatype'] == 'enumeration':
                self.is_enumerated = True
                self.enum_permitted_values = fm.get('display', {}).get(
                    'enum_values', None)
        # Add the data
        self.search_item_row = -1
        self.fill_in_table(None, tag_to_match, ttm_is_first_letter)

        self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.table.customContextMenuRequested.connect(self.show_context_menu)

    def show_context_menu(self, point):
        idx = self.table.indexAt(point)
        if idx.column() != 0:
            return
        m = self.au_context_menu = QMenu(self)

        item = self.table.itemAt(point)
        disable_copy_paste_search = len(
            self.table.selectedItems()) != 1 or item.is_deleted
        ca = m.addAction(_('Copy'))
        ca.triggered.connect(partial(self.copy_to_clipboard, item))
        ca.setIcon(QIcon(I('edit-copy.png')))
        if disable_copy_paste_search:
            ca.setEnabled(False)
        ca = m.addAction(_('Paste'))
        ca.setIcon(QIcon(I('edit-paste.png')))
        ca.triggered.connect(partial(self.paste_from_clipboard, item))
        if disable_copy_paste_search:
            ca.setEnabled(False)
        ca = m.addAction(_('Undo'))
        ca.setIcon(QIcon(I('edit-undo.png')))
        ca.triggered.connect(self.undo_edit)
        ca.setEnabled(False)
        for item in self.table.selectedItems():
            if (item.text() != self.original_names[int(
                    item.data(Qt.ItemDataRole.UserRole))] or item.is_deleted):
                ca.setEnabled(True)
                break
        ca = m.addAction(_('Edit'))
        ca.setIcon(QIcon(I('edit_input.png')))
        ca.triggered.connect(self.rename_tag)
        ca = m.addAction(_('Delete'))
        ca.setIcon(QIcon(I('trash.png')))
        ca.triggered.connect(self.delete_tags)
        item_name = str(item.text())
        ca = m.addAction(_('Search for {}').format(item_name))
        ca.setIcon(QIcon(I('search.png')))
        ca.triggered.connect(partial(self.set_search_text, item_name))
        item_name = str(item.text())
        ca = m.addAction(_('Filter by {}').format(item_name))
        ca.setIcon(QIcon(I('filter.png')))
        ca.triggered.connect(partial(self.set_filter_text, item_name))
        if self.category is not None:
            ca = m.addAction(_("Search the library for {0}").format(item_name))
            ca.setIcon(QIcon(I('lt.png')))
            ca.triggered.connect(partial(self.search_for_books, item))
            if disable_copy_paste_search:
                ca.setEnabled(False)
        if self.table.state() == QAbstractItemView.State.EditingState:
            m.addSeparator()
            case_menu = QMenu(_('Change case'))
            case_menu.setIcon(QIcon(I('font_size_larger.png')))
            action_upper_case = case_menu.addAction(_('Upper case'))
            action_lower_case = case_menu.addAction(_('Lower case'))
            action_swap_case = case_menu.addAction(_('Swap case'))
            action_title_case = case_menu.addAction(_('Title case'))
            action_capitalize = case_menu.addAction(_('Capitalize'))
            action_upper_case.triggered.connect(
                partial(self.do_case, icu_upper))
            action_lower_case.triggered.connect(
                partial(self.do_case, icu_lower))
            action_swap_case.triggered.connect(
                partial(self.do_case, self.swap_case))
            action_title_case.triggered.connect(
                partial(self.do_case, titlecase))
            action_capitalize.triggered.connect(
                partial(self.do_case, capitalize))
            m.addMenu(case_menu)
        m.exec(self.table.mapToGlobal(point))

    def search_for_books(self, item):
        from calibre.gui2.ui import get_gui
        get_gui().search.set_search_string('{}:"={}"'.format(
            self.category,
            str(item.text()).replace(r'"', r'\"')))

        qv = get_quickview_action_plugin()
        if qv:
            view = get_gui().library_view
            rows = view.selectionModel().selectedRows()
            if len(rows) > 0:
                current_row = rows[0].row()
                current_col = view.column_map.index(self.category)
                index = view.model().index(current_row, current_col)
                qv.change_quickview_column(index, show=False)

    def copy_to_clipboard(self, item):
        cb = QApplication.clipboard()
        cb.setText(str(item.text()))

    def paste_from_clipboard(self, item):
        cb = QApplication.clipboard()
        item.setText(cb.text())

    def case_insensitive_compare(self, l, r):
        if prefs['use_primary_find_in_search']:
            return primary_contains(l, r)
        return contains(l.lower(), r.lower())

    def do_case(self, func):
        items = self.table.selectedItems()
        # block signals to avoid the "edit one changes all" behavior
        self.table.blockSignals(True)
        for item in items:
            item.setText(func(str(item.text())))
        self.table.blockSignals(False)

    def swap_case(self, txt):
        return txt.swapcase()

    def vl_box_changed(self):
        self.search_item_row = -1
        self.fill_in_table(None, None, False)

    def do_search(self):
        self.not_found_label.setVisible(False)
        find_text = str(self.search_box.currentText())
        if not find_text:
            return
        for _ in range(0, self.table.rowCount()):
            r = self.search_item_row = (self.search_item_row +
                                        1) % self.table.rowCount()
            if self.string_contains(find_text, self.table.item(r, 0).text()):
                self.table.setCurrentItem(self.table.item(r, 0))
                self.table.setFocus(Qt.FocusReason.OtherFocusReason)
                return
        # Nothing found. Pop up the little dialog for 1.5 seconds
        self.not_found_label.setVisible(True)
        self.not_found_label_timer.start(1500)

    def search_text_changed(self):
        self.search_item_row = -1

    def clear_search(self):
        self.search_item_row = -1
        self.search_box.setText('')

    def set_search_text(self, txt):
        self.search_box.setText(txt)
        self.do_search()

    def fill_in_table(self, tags, tag_to_match, ttm_is_first_letter):
        data = self.get_book_ids(self.apply_vl_checkbox.isChecked())
        self.all_tags = {}
        filter_text = icu_lower(str(self.filter_box.text()))
        for k, v, count in data:
            if not filter_text or self.string_contains(filter_text,
                                                       icu_lower(v)):
                self.all_tags[v] = {
                    'key': k,
                    'count': count,
                    'cur_name': v,
                    'is_deleted': k in self.to_delete
                }
                self.original_names[k] = v
        if self.is_enumerated:
            self.edit_delegate.set_completion_data(self.enum_permitted_values)
        else:
            self.edit_delegate.set_completion_data(
                self.original_names.values())

        self.ordered_tags = sorted(self.all_tags.keys(), key=self.sorter)
        if tags is None:
            tags = self.ordered_tags

        select_item = None
        self.table.blockSignals(True)
        self.table.clear()
        self.table.setColumnCount(3)
        self.name_col = QTableWidgetItem(self.category_name)
        self.table.setHorizontalHeaderItem(0, self.name_col)
        self.count_col = QTableWidgetItem(_('Count'))
        self.table.setHorizontalHeaderItem(1, self.count_col)
        self.was_col = QTableWidgetItem(_('Was'))
        self.table.setHorizontalHeaderItem(2, self.was_col)

        self.table.setRowCount(len(tags))
        for row, tag in enumerate(tags):
            item = NameTableWidgetItem(self.sorter)
            item.set_is_deleted(self.all_tags[tag]['is_deleted'])
            _id = self.all_tags[tag]['key']
            item.setData(Qt.ItemDataRole.UserRole, _id)
            item.set_initial_text(tag)
            if _id in self.to_rename:
                item.setText(self.to_rename[_id])
            else:
                item.setText(tag)
            if self.is_enumerated and str(
                    item.text()) not in self.enum_permitted_values:
                item.setBackground(QColor('#FF2400'))
                item.setToolTip(
                    '<p>' +
                    _("This is not one of this column's permitted values ({0})"
                      ).format(', '.join(self.enum_permitted_values)) + '</p>')
            item.setFlags(item.flags() | Qt.ItemFlag.ItemIsSelectable
                          | Qt.ItemFlag.ItemIsEditable)
            self.table.setItem(row, 0, item)
            if select_item is None:
                if ttm_is_first_letter:
                    if primary_startswith(tag, tag_to_match):
                        select_item = item
                elif tag == tag_to_match:
                    select_item = item
            item = CountTableWidgetItem(self.all_tags[tag]['count'])
            # only the name column can be selected
            item.setFlags(
                item.flags()
                & ~(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable))
            self.table.setItem(row, 1, item)

            item = QTableWidgetItem()
            item.setFlags(
                item.flags()
                & ~(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable))
            if _id in self.to_rename or _id in self.to_delete:
                item.setData(Qt.ItemDataRole.DisplayRole, tag)
            self.table.setItem(row, 2, item)

        if self.last_sorted_by == 'name':
            self.table.sortByColumn(0, self.name_order)
        elif self.last_sorted_by == 'count':
            self.table.sortByColumn(1, self.count_order)
        else:
            self.table.sortByColumn(2, self.was_order)

        if select_item is not None:
            self.table.setCurrentItem(select_item)
            self.table.setFocus(Qt.FocusReason.OtherFocusReason)
            self.start_find_pos = select_item.row()
        else:
            self.table.setCurrentCell(0, 0)
            self.search_box.setFocus()
            self.start_find_pos = -1
        self.table.blockSignals(False)

    def not_found_label_timer_event(self):
        self.not_found_label.setVisible(False)

    def clear_filter(self):
        self.filter_box.setText('')
        self.do_filter()

    def set_filter_text(self, txt):
        self.filter_box.setText(txt)
        self.do_filter()

    def do_filter(self):
        self.fill_in_table(None, None, False)

    def table_column_resized(self, col, old, new):
        self.table_column_widths = []
        for c in range(0, self.table.columnCount()):
            self.table_column_widths.append(self.table.columnWidth(c))

    def resizeEvent(self, *args):
        QDialog.resizeEvent(self, *args)
        if self.table_column_widths is not None:
            for c, w in enumerate(self.table_column_widths):
                self.table.setColumnWidth(c, w)
        else:
            # the vertical scroll bar might not be rendered, so might not yet
            # have a width. Assume 25. Not a problem because user-changed column
            # widths will be remembered
            w = self.table.width() - 25 - self.table.verticalHeader().width()
            w //= self.table.columnCount()
            for c in range(0, self.table.columnCount()):
                self.table.setColumnWidth(c, w)

    def save_geometry(self):
        gprefs['tag_list_editor_table_widths'] = self.table_column_widths
        gprefs['tag_list_editor_dialog_geometry'] = bytearray(
            self.saveGeometry())

    def start_editing(self, on_row):
        items = self.table.selectedItems()
        self.table.blockSignals(True)
        for item in items:
            if item.row() != on_row:
                item.set_placeholder(_('Editing...'))
            else:
                self.text_before_editing = item.text()
        self.table.blockSignals(False)

    def stop_editing(self, on_row):
        items = self.table.selectedItems()
        self.table.blockSignals(True)
        for item in items:
            if item.row() != on_row and item.is_placeholder:
                item.reset_placeholder()
        self.table.blockSignals(False)

    def finish_editing(self, edited_item):
        if not edited_item.text():
            error_dialog(
                self,
                _('Item is blank'),
                _('An item cannot be set to nothing. Delete it instead.'),
                show=True)
            self.table.blockSignals(True)
            edited_item.setText(self.text_before_editing)
            self.table.blockSignals(False)
            return
        new_text = str(edited_item.text())
        if self.is_enumerated and new_text not in self.enum_permitted_values:
            error_dialog(
                self,
                _('Item is not a permitted value'),
                '<p>' +
                _("This column has a fixed set of permitted values. The entered "
                  "text must be one of ({0}).").format(', '.join(
                      self.enum_permitted_values)) + '</p>',
                show=True)
            self.table.blockSignals(True)
            edited_item.setText(self.text_before_editing)
            self.table.blockSignals(False)
            return

        items = self.table.selectedItems()
        self.table.blockSignals(True)
        for item in items:
            id_ = int(item.data(Qt.ItemDataRole.UserRole))
            self.to_rename[id_] = new_text
            orig = self.table.item(item.row(), 2)
            item.setText(new_text)
            orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
        self.table.blockSignals(False)

    def undo_edit(self):
        indexes = self.table.selectionModel().selectedRows()
        if not indexes:
            error_dialog(
                self, _('No item selected'),
                _('You must select one item from the list of available items.')
            ).exec()
            return

        if not confirm(_('Do you really want to undo your changes?'),
                       'tag_list_editor_undo'):
            return
        self.table.blockSignals(True)
        for idx in indexes:
            row = idx.row()
            item = self.table.item(row, 0)
            item.setText(item.initial_text())
            item.set_is_deleted(False)
            self.to_delete.discard(int(item.data(Qt.ItemDataRole.UserRole)))
            self.to_rename.pop(int(item.data(Qt.ItemDataRole.UserRole)), None)
            self.table.item(row, 2).setData(Qt.ItemDataRole.DisplayRole, '')
        self.table.blockSignals(False)

    def rename_tag(self):
        item = self.table.item(self.table.currentRow(), 0)
        self._rename_tag(item)

    def _rename_tag(self, item):
        if item is None:
            error_dialog(
                self, _('No item selected'),
                _('You must select one item from the list of available items.')
            ).exec()
            return
        for col_zero_item in self.table.selectedItems():
            if col_zero_item.is_deleted:
                if not question_dialog(
                        self, _('Undelete items?'), '<p>' +
                        _('Items must be undeleted to continue. Do you want '
                          'to do this?') + '<br>'):
                    return
        self.table.blockSignals(True)
        for col_zero_item in self.table.selectedItems():
            # undelete any deleted items
            if col_zero_item.is_deleted:
                col_zero_item.set_is_deleted(False)
                self.to_delete.discard(
                    int(col_zero_item.data(Qt.ItemDataRole.UserRole)))
                orig = self.table.item(col_zero_item.row(), 2)
                orig.setData(Qt.ItemDataRole.DisplayRole, '')
        self.table.blockSignals(False)
        self.table.editItem(item)

    def delete_pressed(self):
        if self.table.currentColumn() == 0:
            self.delete_tags()

    def delete_tags(self):
        deletes = self.table.selectedItems()
        if not deletes:
            error_dialog(
                self, _('No items selected'),
                _('You must select at least one item from the list.')).exec()
            return

        to_del = []
        for item in deletes:
            if not item.is_deleted:
                to_del.append(item)

        if to_del:
            ct = ', '.join([str(item.text()) for item in to_del])
            if not confirm(
                    '<p>' +
                    _('Are you sure you want to delete the following items?') +
                    '<br>' + ct, 'tag_list_editor_delete'):
                return

        row = self.table.row(deletes[0])
        self.table.blockSignals(True)
        for item in deletes:
            id_ = int(item.data(Qt.ItemDataRole.UserRole))
            self.to_delete.add(id_)
            item.set_is_deleted(True)
            orig = self.table.item(item.row(), 2)
            orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
        self.table.blockSignals(False)
        if row >= self.table.rowCount():
            row = self.table.rowCount() - 1
        if row >= 0:
            self.table.scrollToItem(self.table.item(row, 0))

    def do_sort(self, section):
        (self.do_sort_by_name, self.do_sort_by_count,
         self.do_sort_by_was)[section]()

    def do_sort_by_name(self):
        self.name_order = 1 - self.name_order
        self.last_sorted_by = 'name'
        self.table.sortByColumn(0, self.name_order)

    def do_sort_by_count(self):
        self.count_order = 1 - self.count_order
        self.last_sorted_by = 'count'
        self.table.sortByColumn(1, self.count_order)

    def do_sort_by_was(self):
        self.was_order = 1 - self.was_order
        self.last_sorted_by = 'count'
        self.table.sortByColumn(2, self.was_order)

    def accepted(self):
        self.save_geometry()
예제 #10
0
class DiffView(QWidget):  # {{{

    SYNC_POSITION = 0.4
    line_activated = pyqtSignal(object, object, object)

    def __init__(self, parent=None, show_open_in_editor=False):
        QWidget.__init__(self, parent)
        self.changes = [[], [], []]
        self.delta = 0
        self.l = l = QHBoxLayout(self)
        self.setLayout(l)
        self.syncpos = 0
        l.setContentsMargins(0, 0, 0, 0), l.setSpacing(0)
        self.view = DiffSplit(self, show_open_in_editor=show_open_in_editor)
        l.addWidget(self.view)
        self.add_diff = self.view.add_diff
        self.scrollbar = QScrollBar(self)
        l.addWidget(self.scrollbar)
        self.syncing = False
        self.bars = []
        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.resize_timer.timeout.connect(self.resize_debounced)
        for bar in (self.scrollbar, self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar()):
            self.bars.append(bar)
            bar.scroll_idx = len(self.bars) - 1
            connect_lambda(bar.valueChanged[int], self, lambda self: self.scrolled(self.sender().scroll_idx))
        self.view.left.resized.connect(self.resized)
        for v in (self.view.left, self.view.right, self.view.handle(1)):
            v.wheel_event.connect(self.scrollbar.wheelEvent)
            if v is self.view.left or v is self.view.right:
                v.next_change.connect(self.next_change)
                v.line_activated.connect(self.line_activated)
                connect_lambda(v.scrolled, self,
                        lambda self: self.scrolled(1 if self.sender() is self.view.left else 2))

    def next_change(self, delta):
        assert delta in (1, -1)
        position = self.get_position_from_scrollbar(0)
        if position[0] == 'in':
            p = n = position[1]
        else:
            p, n = position[1], position[1] + 1
            if p < 0:
                p = None
            if n >= len(self.changes[0]):
                n = None
        if p == n:
            nc = p + delta
            if nc < 0 or nc >= len(self.changes[0]):
                nc = None
        else:
            nc = {1:n, -1:p}[delta]
        if nc is None:
            self.scrollbar.setValue(0 if delta == -1 else self.scrollbar.maximum())
        else:
            val = self.scrollbar.value()
            self.scroll_to(0, ('in', nc, 0))
            nval = self.scrollbar.value()
            if nval == val:
                nval += 5 * delta
                if 0 <= nval <= self.scrollbar.maximum():
                    self.scrollbar.setValue(nval)

    def resized(self):
        self.resize_timer.start(300)

    def resize_debounced(self):
        self.view.resized()
        self.calculate_length()
        self.adjust_range()
        self.view.handle(1).update()

    def get_position_from_scrollbar(self, which):
        changes = self.changes[which]
        bar = self.bars[which]
        syncpos = self.syncpos + bar.value()
        prev = 0
        for i, (top, bot, kind) in enumerate(changes):
            if syncpos <= bot:
                if top <= syncpos:
                    # syncpos is inside a change
                    try:
                        ratio = float(syncpos - top) / (bot - top)
                    except ZeroDivisionError:
                        ratio = 0
                    return 'in', i, ratio
                else:
                    # syncpos is after the previous change
                    offset = syncpos - prev
                    return 'after', i - 1, offset
            else:
                # syncpos is after the current change
                prev = bot
        offset = syncpos - prev
        return 'after', len(changes) - 1, offset

    def scroll_to(self, which, position):
        changes = self.changes[which]
        bar = self.bars[which]
        val = None
        if position[0] == 'in':
            change_idx, ratio = position[1:]
            start, end = changes[change_idx][:2]
            val = start + int((end - start) * ratio)
        else:
            change_idx, offset = position[1:]
            start = 0 if change_idx < 0 else changes[change_idx][1]
            val = start + offset
        bar.setValue(val - self.syncpos)

    def scrolled(self, which, *args):
        if self.syncing:
            return
        position = self.get_position_from_scrollbar(which)
        with self:
            for x in {0, 1, 2} - {which}:
                self.scroll_to(x, position)
        self.view.handle(1).update()

    def __enter__(self):
        self.syncing = True

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

    def clear(self):
        with self:
            self.view.clear()
            self.changes = [[], [], []]
            self.delta = 0
            self.scrollbar.setRange(0, 0)

    def adjust_range(self):
        ls, rs = self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar()
        self.scrollbar.setPageStep(min(ls.pageStep(), rs.pageStep()))
        self.scrollbar.setSingleStep(min(ls.singleStep(), rs.singleStep()))
        self.scrollbar.setRange(0, ls.maximum() + self.delta)
        self.scrollbar.setVisible(self.view.left.document().lineCount() > ls.pageStep() or self.view.right.document().lineCount() > rs.pageStep())
        self.syncpos = int(ceil(self.scrollbar.pageStep() * self.SYNC_POSITION))

    def finalize(self):
        self.view.finalize()
        self.changes = [[], [], []]
        self.calculate_length()
        self.adjust_range()

    def calculate_length(self):
        delta = 0
        line_number_changes = ([], [])
        for v, lmap, changes in zip((self.view.left, self.view.right), ({}, {}), line_number_changes):
            b = v.document().firstBlock()
            ebl = v.document().documentLayout().ensureBlockLayout
            last_line_count = 0
            while b.isValid():
                ebl(b)
                lmap[b.blockNumber()] = last_line_count
                last_line_count += b.layout().lineCount()
                b = b.next()
            for top, bot, kind in v.changes:
                changes.append((lmap[top], lmap[bot], kind))

        changes = []
        for (l_top, l_bot, kind), (r_top, r_bot, kind) in zip(*line_number_changes):
            height = max(l_bot - l_top, r_bot - r_top)
            top = delta + l_top
            changes.append((top, top + height, kind))
            delta = top + height - l_bot
        self.changes, self.delta = (changes,) + line_number_changes, delta

    def handle_key(self, ev):
        amount, d = None, 1
        key = ev.key()
        if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_J, Qt.Key.Key_K):
            amount = self.scrollbar.singleStep()
            if key in (Qt.Key.Key_Up, Qt.Key.Key_K):
                d = -1
        elif key in (Qt.Key.Key_PageUp, Qt.Key.Key_PageDown):
            amount = self.scrollbar.pageStep()
            if key in (Qt.Key.Key_PageUp,):
                d = -1
        elif key in (Qt.Key.Key_Home, Qt.Key.Key_End):
            self.scrollbar.setValue(0 if key == Qt.Key.Key_Home else self.scrollbar.maximum())
            return True
        elif key in (Qt.Key.Key_N, Qt.Key.Key_P):
            self.next_change(1 if key == Qt.Key.Key_N else -1)
            return True

        if amount is not None:
            self.scrollbar.setValue(self.scrollbar.value() + d * amount)
            return True
        return False
예제 #11
0
class LiveCSS(QWidget):

    goto_declaration = pyqtSignal(object)

    def __init__(self, preview, parent=None):
        QWidget.__init__(self, parent)
        self.preview = preview
        preview.live_css_data.connect(self.got_live_css_data)
        self.preview_is_refreshing = False
        self.refresh_needed = False
        preview.refresh_starting.connect(self.preview_refresh_starting)
        preview.refreshed.connect(self.preview_refreshed)
        self.apply_theme()
        self.setAutoFillBackground(True)
        self.update_timer = QTimer(self)
        self.update_timer.timeout.connect(self.update_data)
        self.update_timer.setSingleShot(True)
        self.update_timer.setInterval(500)
        self.now_showing = (None, None, None)

        self.stack = s = QStackedLayout(self)
        self.setLayout(s)

        self.clear_label = la = QLabel(
            '<h3>' + _('No style information found') + '</h3><p>' +
            _('Move the cursor inside a HTML tag to see what styles'
              ' apply to that tag.'))
        la.setWordWrap(True)
        la.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
        s.addWidget(la)

        self.box = box = Box(self)
        box.hyperlink_activated.connect(
            self.goto_declaration, type=Qt.ConnectionType.QueuedConnection)
        self.scroll = sc = QScrollArea(self)
        sc.setWidget(box)
        sc.setWidgetResizable(True)
        s.addWidget(sc)

    def preview_refresh_starting(self):
        self.preview_is_refreshing = True

    def preview_refreshed(self):
        self.preview_is_refreshing = False
        self.refresh_needed = True
        self.start_update_timer()

    def apply_theme(self):
        f = self.font()
        f.setFamily(tprefs['editor_font_family'] or default_font_family())
        f.setPointSizeF(tprefs['editor_font_size'])
        self.setFont(f)
        theme = get_theme(tprefs['editor_theme'])
        pal = self.palette()
        pal.setColor(QPalette.ColorRole.Window,
                     theme_color(theme, 'Normal', 'bg'))
        pal.setColor(QPalette.ColorRole.WindowText,
                     theme_color(theme, 'Normal', 'fg'))
        pal.setColor(QPalette.ColorRole.AlternateBase,
                     theme_color(theme, 'HighlightRegion', 'bg'))
        pal.setColor(QPalette.ColorRole.Link, theme_color(theme, 'Link', 'fg'))
        pal.setColor(QPalette.ColorRole.LinkVisited,
                     theme_color(theme, 'Keyword', 'fg'))
        self.setPalette(pal)
        if hasattr(self, 'box'):
            self.box.relayout()
        self.update()

    def clear(self):
        self.stack.setCurrentIndex(0)

    def show_data(self, editor_name, sourceline, tags):
        if self.preview_is_refreshing:
            return
        if sourceline is None:
            self.clear()
        else:
            self.preview.request_live_css_data(editor_name, sourceline, tags)

    def got_live_css_data(self, result):
        maximum_specificities = {}
        for node in result['nodes']:
            for rule in node['css']:
                self.process_rule(rule, node['ancestor_specificity'],
                                  maximum_specificities)
        for node in result['nodes']:
            for rule in node['css']:
                for prop in rule['properties']:
                    if prop.specificity < maximum_specificities[prop.name]:
                        prop.is_overriden = True
        self.display_received_live_css_data(result)

    def display_received_live_css_data(self, data):
        editor_name = data['editor_name']
        sourceline = data['sourceline']
        tags = data['tags']
        if data is None or len(data['computed_css']) < 1:
            if editor_name == self.current_name and (editor_name, sourceline,
                                                     tags) == self.now_showing:
                # Try again in a little while in case there was a transient
                # error in the web view
                self.start_update_timer()
                return
            self.clear()
            return
        self.now_showing = (editor_name, sourceline, tags)
        data['html_name'] = editor_name
        self.box.show_data(data)
        self.refresh_needed = False
        self.stack.setCurrentIndex(1)

    def process_rule(self, rule, ancestor_specificity, maximum_specificities):
        selector = rule['selector']
        sheet_index = rule['sheet_index']
        rule_address = rule['rule_address'] or ()
        if selector is not None:
            try:
                specificity = [0] + list(parse(selector)[0].specificity())
            except (AttributeError, TypeError, SelectorError):
                specificity = [0, 0, 0, 0]
        else:  # style attribute
            specificity = [1, 0, 0, 0]
        specificity.extend((sheet_index, tuple(rule_address)))
        properties = []
        for prop in rule['properties']:
            important = 1 if prop[-1] == 'important' else 0
            p = Property(prop,
                         [ancestor_specificity] + [important] + specificity)
            properties.append(p)
            if p.specificity > maximum_specificities.get(
                    p.name, lowest_specificity):
                maximum_specificities[p.name] = p.specificity
        rule['properties'] = properties

        href = rule['href']
        if hasattr(href, 'startswith') and href.startswith(
                f'{FAKE_PROTOCOL}://{FAKE_HOST}'):
            qurl = QUrl(href)
            name = qurl.path()[1:]
            if name:
                rule['href'] = name

    @property
    def current_name(self):
        return self.preview.current_name

    @property
    def is_visible(self):
        return self.isVisible()

    def showEvent(self, ev):
        self.update_timer.start()
        actions['auto-reload-preview'].setEnabled(True)
        return QWidget.showEvent(self, ev)

    def sync_to_editor(self):
        self.update_data()

    def update_data(self):
        if not self.is_visible or self.preview_is_refreshing:
            return
        editor_name = self.current_name
        ed = editors.get(editor_name, None)
        if self.update_timer.isActive() or (ed is None
                                            and editor_name is not None):
            return QTimer.singleShot(100, self.update_data)
        if ed is not None:
            sourceline, tags = ed.current_tag(for_position_sync=False)
            if self.refresh_needed or self.now_showing != (editor_name,
                                                           sourceline, tags):
                self.show_data(editor_name, sourceline, tags)

    def start_update_timer(self):
        if self.is_visible:
            self.update_timer.start()

    def stop_update_timer(self):
        self.update_timer.stop()

    def navigate_to_declaration(self, data, editor):
        if data['type'] == 'inline':
            sourceline, tags = data['sourceline_address']
            editor.goto_sourceline(sourceline, tags, attribute='style')
        elif data['type'] == 'sheet':
            editor.goto_css_rule(data['rule_address'])
        elif data['type'] == 'elem':
            editor.goto_css_rule(data['rule_address'],
                                 sourceline_address=data['sourceline_address'])
예제 #12
0
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
    def __init__(self,
                 parent,
                 db,
                 id_to_select,
                 select_sort,
                 select_link,
                 find_aut_func,
                 is_first_letter=False):
        QDialog.__init__(self, parent)
        Ui_EditAuthorsDialog.__init__(self)
        self.setupUi(self)

        # Remove help icon on title bar
        icon = self.windowIcon()
        self.setWindowFlags(self.windowFlags()
                            & (~Qt.WindowType.WindowContextHelpButtonHint))
        self.setWindowIcon(icon)

        try:
            self.table_column_widths = \
                        gprefs.get('manage_authors_table_widths', None)
            geom = gprefs.get('manage_authors_dialog_geometry', None)
            if geom:
                QApplication.instance().safe_restore_geometry(
                    self, QByteArray(geom))
        except Exception:
            pass

        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(
            _('&OK'))
        self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(
            _('&Cancel'))
        self.buttonBox.accepted.connect(self.accepted)
        self.apply_vl_checkbox.stateChanged.connect(self.use_vl_changed)

        # Set up the heading for sorting
        self.table.setSelectionMode(
            QAbstractItemView.SelectionMode.SingleSelection)

        self.find_aut_func = find_aut_func
        self.table.resizeColumnsToContents()
        if self.table.columnWidth(2) < 200:
            self.table.setColumnWidth(2, 200)

        # set up the cellChanged signal only after the table is filled
        self.table.cellChanged.connect(self.cell_changed)

        self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort)
        self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author)

        # Capture clicks on the horizontal header to sort the table columns
        hh = self.table.horizontalHeader()
        hh.sectionResized.connect(self.table_column_resized)
        hh.setSectionsClickable(True)
        hh.sectionClicked.connect(self.do_sort)
        hh.setSortIndicatorShown(True)

        # set up the search & filter boxes
        self.find_box.initialize('manage_authors_search')
        le = self.find_box.lineEdit()
        ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
        if ac is not None:
            ac.triggered.connect(self.clear_find)
        le.returnPressed.connect(self.do_find)
        self.find_box.editTextChanged.connect(self.find_text_changed)
        self.find_button.clicked.connect(self.do_find)
        self.find_button.setDefault(True)

        self.filter_box.initialize('manage_authors_filter')
        le = self.filter_box.lineEdit()
        ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
        if ac is not None:
            ac.triggered.connect(self.clear_filter)
        self.filter_box.lineEdit().returnPressed.connect(self.do_filter)
        self.filter_button.clicked.connect(self.do_filter)

        self.not_found_label = l = QLabel(self.table)
        l.setFrameStyle(QFrame.Shape.StyledPanel)
        l.setAutoFillBackground(True)
        l.setText(_('No matches found'))
        l.setAlignment(Qt.AlignmentFlag.AlignVCenter)
        l.resize(l.sizeHint())
        l.move(10, 2)
        l.setVisible(False)
        self.not_found_label_timer = QTimer()
        self.not_found_label_timer.setSingleShot(True)
        self.not_found_label_timer.timeout.connect(
            self.not_found_label_timer_event,
            type=Qt.ConnectionType.QueuedConnection)

        self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.table.customContextMenuRequested.connect(self.show_context_menu)

        # Fetch the data
        self.authors = {}
        self.original_authors = {}
        auts = db.new_api.author_data()
        self.completion_data = []
        for id_, v in auts.items():
            name = v['name']
            name = name.replace('|', ',')
            self.completion_data.append(name)
            self.authors[id_] = {
                'name': name,
                'sort': v['sort'],
                'link': v['link']
            }
            self.original_authors[id_] = {
                'name': name,
                'sort': v['sort'],
                'link': v['link']
            }

        self.edited_icon = QIcon(I('modified.png'))
        self.empty_icon = QIcon()
        if prefs['use_primary_find_in_search']:
            self.string_contains = primary_contains
        else:
            self.string_contains = contains

        self.last_sorted_by = 'sort'
        self.author_order = 1
        self.author_sort_order = 0
        self.link_order = 1
        self.show_table(id_to_select, select_sort, select_link,
                        is_first_letter)

    def use_vl_changed(self, x):
        self.show_table(None, None, None, False)

    def clear_filter(self):
        self.filter_box.setText('')
        self.show_table(None, None, None, False)

    def do_filter(self):
        self.show_table(None, None, None, False)

    def show_table(self, id_to_select, select_sort, select_link,
                   is_first_letter):
        auts_to_show = {
            t[0]
            for t in self.find_aut_func(
                use_virtual_library=self.apply_vl_checkbox.isChecked())
        }
        filter_text = icu_lower(unicode_type(self.filter_box.text()))
        if filter_text:
            auts_to_show = {
                id_
                for id_ in auts_to_show if self.string_contains(
                    filter_text, icu_lower(self.authors[id_]['name']))
            }

        self.table.blockSignals(True)
        self.table.clear()
        self.table.setColumnCount(3)

        self.table.setRowCount(len(auts_to_show))
        row = 0
        for id_, v in self.authors.items():
            if id_ not in auts_to_show:
                continue
            name, sort, link = (v['name'], v['sort'], v['link'])
            name = name.replace('|', ',')

            name_item = tableItem(name)
            name_item.setData(Qt.ItemDataRole.UserRole, id_)
            sort_item = tableItem(sort)
            link_item = tableItem(link)

            self.table.setItem(row, 0, name_item)
            self.table.setItem(row, 1, sort_item)
            self.table.setItem(row, 2, link_item)

            self.set_icon(name_item, id_)
            self.set_icon(sort_item, id_)
            self.set_icon(link_item, id_)
            row += 1

        self.table.setItemDelegate(EditColumnDelegate(self.completion_data))
        self.table.setHorizontalHeaderLabels(
            [_('Author'), _('Author sort'),
             _('Link')])

        if self.last_sorted_by == 'sort':
            self.author_sort_order = 1 - self.author_sort_order
            self.do_sort_by_author_sort()
        elif self.last_sorted_by == 'author':
            self.author_order = 1 - self.author_order
            self.do_sort_by_author()
        else:
            self.link_order = 1 - self.link_order
            self.do_sort_by_link()

        # Position on the desired item
        select_item = None
        if id_to_select:
            use_as = tweaks[
                'categories_use_field_for_author_name'] == 'author_sort'
            for row in range(0, len(auts_to_show)):
                if is_first_letter:
                    item_txt = unicode_type(
                        self.table.item(row, 1).text() if use_as else self.
                        table.item(row, 0).text())
                    if primary_startswith(item_txt, id_to_select):
                        select_item = self.table.item(row, 1 if use_as else 0)
                        break
                elif id_to_select == self.table.item(row, 0).data(
                        Qt.ItemDataRole.UserRole):
                    if select_sort:
                        select_item = self.table.item(row, 1)
                    elif select_link:
                        select_item = self.table.item(row, 2)
                    else:
                        select_item = (self.table.item(row, 1)
                                       if use_as else self.table.item(row, 0))
                    break
        if select_item:
            self.table.setCurrentItem(select_item)
            self.table.setFocus(True)
            if select_sort or select_link:
                self.table.editItem(select_item)
            self.start_find_pos = select_item.row() * 2 + select_item.column()
        else:
            self.table.setCurrentCell(0, 0)
            self.find_box.setFocus()
            self.start_find_pos = -1
        self.table.blockSignals(False)

    def save_state(self):
        self.table_column_widths = []
        for c in range(0, self.table.columnCount()):
            self.table_column_widths.append(self.table.columnWidth(c))
        gprefs['manage_authors_table_widths'] = self.table_column_widths
        gprefs['manage_authors_dialog_geometry'] = bytearray(
            self.saveGeometry())

    def table_column_resized(self, col, old, new):
        self.table_column_widths = []
        for c in range(0, self.table.columnCount()):
            self.table_column_widths.append(self.table.columnWidth(c))

    def resizeEvent(self, *args):
        QDialog.resizeEvent(self, *args)
        if self.table_column_widths is not None:
            for c, w in enumerate(self.table_column_widths):
                self.table.setColumnWidth(c, w)
        else:
            # the vertical scroll bar might not be rendered, so might not yet
            # have a width. Assume 25. Not a problem because user-changed column
            # widths will be remembered
            w = self.table.width() - 25 - self.table.verticalHeader().width()
            w //= self.table.columnCount()
            for c in range(0, self.table.columnCount()):
                self.table.setColumnWidth(c, w)
        self.save_state()

    def get_column_name(self, column):
        return ['name', 'sort', 'link'][column]

    def show_context_menu(self, point):
        self.context_item = self.table.itemAt(point)
        case_menu = QMenu(_('Change case'))
        action_upper_case = case_menu.addAction(_('Upper case'))
        action_lower_case = case_menu.addAction(_('Lower case'))
        action_swap_case = case_menu.addAction(_('Swap case'))
        action_title_case = case_menu.addAction(_('Title case'))
        action_capitalize = case_menu.addAction(_('Capitalize'))

        action_upper_case.triggered.connect(self.upper_case)
        action_lower_case.triggered.connect(self.lower_case)
        action_swap_case.triggered.connect(self.swap_case)
        action_title_case.triggered.connect(self.title_case)
        action_capitalize.triggered.connect(self.capitalize)

        m = self.au_context_menu = QMenu(self)
        idx = self.table.indexAt(point)
        id_ = int(self.table.item(idx.row(), 0).data(Qt.ItemDataRole.UserRole))
        sub = self.get_column_name(idx.column())
        if self.context_item.text() != self.original_authors[id_][sub]:
            ca = m.addAction(_('Undo'))
            ca.triggered.connect(
                partial(self.undo_cell,
                        old_value=self.original_authors[id_][sub]))
            m.addSeparator()
        ca = m.addAction(_('Copy'))
        ca.triggered.connect(self.copy_to_clipboard)
        ca = m.addAction(_('Paste'))
        ca.triggered.connect(self.paste_from_clipboard)
        m.addSeparator()
        if self.context_item is not None and self.context_item.column() == 0:
            ca = m.addAction(_('Copy to author sort'))
            ca.triggered.connect(self.copy_au_to_aus)
            m.addSeparator()
            ca = m.addAction(_("Show books by author in book list"))
            ca.triggered.connect(self.search_in_book_list)
        else:
            ca = m.addAction(_('Copy to author'))
            ca.triggered.connect(self.copy_aus_to_au)
        m.addSeparator()
        m.addMenu(case_menu)
        m.exec_(self.table.mapToGlobal(point))

    def undo_cell(self, old_value):
        self.context_item.setText(old_value)

    def search_in_book_list(self):
        from calibre.gui2.ui import get_gui
        row = self.context_item.row()
        get_gui().search.set_search_string(
            'authors:="%s"' %
            unicode_type(self.table.item(row, 0).text()).replace(r'"', r'\"'))

    def copy_to_clipboard(self):
        cb = QApplication.clipboard()
        cb.setText(unicode_type(self.context_item.text()))

    def paste_from_clipboard(self):
        cb = QApplication.clipboard()
        self.context_item.setText(cb.text())

    def upper_case(self):
        self.context_item.setText(
            icu_upper(unicode_type(self.context_item.text())))

    def lower_case(self):
        self.context_item.setText(
            icu_lower(unicode_type(self.context_item.text())))

    def swap_case(self):
        self.context_item.setText(
            unicode_type(self.context_item.text()).swapcase())

    def title_case(self):
        from calibre.utils.titlecase import titlecase
        self.context_item.setText(
            titlecase(unicode_type(self.context_item.text())))

    def capitalize(self):
        from calibre.utils.icu import capitalize
        self.context_item.setText(
            capitalize(unicode_type(self.context_item.text())))

    def copy_aus_to_au(self):
        row = self.context_item.row()
        dest = self.table.item(row, 0)
        dest.setText(self.context_item.text())

    def copy_au_to_aus(self):
        row = self.context_item.row()
        dest = self.table.item(row, 1)
        dest.setText(self.context_item.text())

    def not_found_label_timer_event(self):
        self.not_found_label.setVisible(False)

    def clear_find(self):
        self.find_box.setText('')
        self.start_find_pos = -1
        self.do_find()

    def find_text_changed(self):
        self.start_find_pos = -1

    def do_find(self):
        self.not_found_label.setVisible(False)
        # For some reason the button box keeps stealing the RETURN shortcut.
        # Steal it back
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Ok).setDefault(False)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Ok).setAutoDefault(False)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setDefault(False)
        self.buttonBox.button(
            QDialogButtonBox.StandardButton.Cancel).setAutoDefault(False)

        st = icu_lower(unicode_type(self.find_box.currentText()))
        if not st:
            return
        for _ in range(0, self.table.rowCount() * 2):
            self.start_find_pos = (self.start_find_pos +
                                   1) % (self.table.rowCount() * 2)
            r = (self.start_find_pos // 2) % self.table.rowCount()
            c = self.start_find_pos % 2
            item = self.table.item(r, c)
            text = icu_lower(unicode_type(item.text()))
            if st in text:
                self.table.setCurrentItem(item)
                self.table.setFocus(True)
                return
        # Nothing found. Pop up the little dialog for 1.5 seconds
        self.not_found_label.setVisible(True)
        self.not_found_label_timer.start(1500)

    def do_sort(self, section):
        (self.do_sort_by_author, self.do_sort_by_author_sort,
         self.do_sort_by_link)[section]()

    def do_sort_by_author(self):
        self.last_sorted_by = 'author'
        self.author_order = 1 - self.author_order
        self.table.sortByColumn(0, self.author_order)

    def do_sort_by_author_sort(self):
        self.last_sorted_by = 'sort'
        self.author_sort_order = 1 - self.author_sort_order
        self.table.sortByColumn(1, self.author_sort_order)

    def do_sort_by_link(self):
        self.last_sorted_by = 'link'
        self.link_order = 1 - self.link_order
        self.table.sortByColumn(2, self.link_order)

    def accepted(self):
        self.save_state()
        self.result = []
        for id_, v in self.authors.items():
            orig = self.original_authors[id_]
            if orig != v:
                self.result.append(
                    (id_, orig['name'], v['name'], v['sort'], v['link']))

    def do_recalc_author_sort(self):
        self.table.cellChanged.disconnect()
        for row in range(0, self.table.rowCount()):
            item_aut = self.table.item(row, 0)
            id_ = int(item_aut.data(Qt.ItemDataRole.UserRole))
            aut = unicode_type(item_aut.text()).strip()
            item_aus = self.table.item(row, 1)
            # Sometimes trailing commas are left by changing between copy algs
            aus = unicode_type(author_to_author_sort(aut)).rstrip(',')
            item_aus.setText(aus)
            self.authors[id_]['sort'] = aus
            self.set_icon(item_aus, id_)
        self.table.setFocus(Qt.FocusReason.OtherFocusReason)
        self.table.cellChanged.connect(self.cell_changed)

    def do_auth_sort_to_author(self):
        self.table.cellChanged.disconnect()
        for row in range(0, self.table.rowCount()):
            aus = unicode_type(self.table.item(row, 1).text()).strip()
            item_aut = self.table.item(row, 0)
            id_ = int(item_aut.data(Qt.ItemDataRole.UserRole))
            item_aut.setText(aus)
            self.authors[id_]['name'] = aus
            self.set_icon(item_aut, id_)
        self.table.setFocus(Qt.FocusReason.OtherFocusReason)
        self.table.cellChanged.connect(self.cell_changed)

    def set_icon(self, item, id_):
        col_name = self.get_column_name(item.column())
        if unicode_type(item.text()) != self.original_authors[id_][col_name]:
            item.setIcon(self.edited_icon)
        else:
            item.setIcon(self.empty_icon)

    def cell_changed(self, row, col):
        id_ = int(self.table.item(row, 0).data(Qt.ItemDataRole.UserRole))
        if col == 0:
            item = self.table.item(row, 0)
            aut = unicode_type(item.text()).strip()
            aut_list = string_to_authors(aut)
            if len(aut_list) != 1:
                error_dialog(
                    self.parent(), _('Invalid author name'),
                    _('You cannot change an author to multiple authors.')
                ).exec_()
                aut = ' % '.join(aut_list)
                self.table.item(row, 0).setText(aut)
            item.set_sort_key()
            self.authors[id_]['name'] = aut
            self.set_icon(item, id_)
            c = self.table.item(row, 1)
            txt = author_to_author_sort(aut)
            self.authors[id_]['sort'] = txt
            c.setText(txt)  # This triggers another cellChanged event
            item = c
        else:
            item = self.table.item(row, col)
            item.set_sort_key()
            self.set_icon(item, id_)
            self.authors[id_][self.get_column_name(col)] = unicode_type(
                item.text())
        self.table.setCurrentItem(item)
        self.table.scrollToItem(item)
예제 #13
0
파일: search.py 프로젝트: qykth-git/calibre
class SearchDialog(QDialog, Ui_Dialog):

    SEARCH_TEXT = _('&Search')
    STOP_TEXT = _('&Stop')

    def __init__(self, gui, parent=None, query=''):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        s = self.style()
        self.close.setIcon(
            s.standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton))

        self.config = JSONConfig('store/search')
        self.search_title.initialize('store_search_search_title')
        self.search_author.initialize('store_search_search_author')
        self.search_edit.initialize('store_search_search')

        # Loads variables that store various settings.
        # This needs to be called soon in __init__ because
        # the variables it sets up are used later.
        self.load_settings()

        self.gui = gui

        # Setup our worker threads.
        self.search_pool = SearchThreadPool(self.search_thread_count)
        self.cache_pool = CacheUpdateThreadPool(self.cache_thread_count)
        self.results_view.model().cover_pool.set_thread_count(
            self.cover_thread_count)
        self.results_view.model().details_pool.set_thread_count(
            self.details_thread_count)
        self.results_view.setCursor(Qt.CursorShape.PointingHandCursor)

        # Check for results and hung threads.
        self.checker = QTimer()
        self.progress_checker = QTimer()
        self.hang_check = 0

        # Update store caches silently.
        for p in self.gui.istores.values():
            self.cache_pool.add_task(p, self.timeout)

        self.store_checks = {}
        self.setup_store_checks()

        # Set the search query
        if isinstance(query, (bytes, str)):
            self.search_edit.setText(query)
        elif isinstance(query, dict):
            if 'author' in query:
                self.search_author.setText(query['author'])
            if 'title' in query:
                self.search_title.setText(query['title'])

        # Create and add the progress indicator
        self.pi = ProgressIndicator(self, 24)
        self.button_layout.takeAt(0)
        self.button_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.button_layout.insertWidget(0, self.pi, 0,
                                        Qt.AlignmentFlag.AlignCenter)

        self.adv_search_button.setIcon(QIcon(I('gear.png')))
        self.adv_search_button.setToolTip(_('Advanced search'))
        self.configure.setIcon(QIcon(I('config.png')))

        self.adv_search_button.clicked.connect(self.build_adv_search)
        self.search.clicked.connect(self.toggle_search)
        self.checker.timeout.connect(self.get_results)
        self.progress_checker.timeout.connect(self.check_progress)
        self.results_view.activated.connect(self.result_item_activated)
        self.results_view.download_requested.connect(self.download_book)
        self.results_view.open_requested.connect(self.open_store)
        self.results_view.model().total_changed.connect(self.update_book_total)
        self.select_all_stores.clicked.connect(self.stores_select_all)
        self.select_invert_stores.clicked.connect(self.stores_select_invert)
        self.select_none_stores.clicked.connect(self.stores_select_none)
        self.configure.clicked.connect(self.do_config)
        self.finished.connect(self.dialog_closed)
        self.searching = False

        self.progress_checker.start(100)

        self.restore_state()

    def setup_store_checks(self):
        first_run = self.config.get('first_run', True)

        # Add check boxes for each store so the user
        # can disable searching specific stores on a
        # per search basis.
        existing = {}
        for n in self.store_checks:
            existing[n] = self.store_checks[n].isChecked()

        self.store_checks = {}

        stores_check_widget = QWidget()
        store_list_layout = QGridLayout()
        stores_check_widget.setLayout(store_list_layout)

        icon = QIcon(I('donate.png'))
        for i, x in enumerate(
                sorted(self.gui.istores.keys(), key=lambda x: x.lower())):
            cbox = QCheckBox(x)
            cbox.setChecked(existing.get(x, first_run))
            store_list_layout.addWidget(cbox, i, 0, 1, 1)
            if self.gui.istores[x].base_plugin.affiliate:
                iw = QLabel(self)
                iw.setToolTip('<p>' + _(
                    'Buying from this store supports the calibre developer: %s</p>'
                ) % self.gui.istores[x].base_plugin.author + '</p>')
                iw.setPixmap(icon.pixmap(16, 16))
                store_list_layout.addWidget(iw, i, 1, 1, 1)
            self.store_checks[x] = cbox
        store_list_layout.setRowStretch(store_list_layout.rowCount(), 10)
        self.store_list.setWidget(stores_check_widget)

        self.config['first_run'] = False

    def build_adv_search(self):
        adv = AdvSearchBuilderDialog(self)
        if adv.exec() == QDialog.DialogCode.Accepted:
            self.search_edit.setText(adv.search_string())

    def resize_columns(self):
        total = 600
        # Cover
        self.results_view.setColumnWidth(0, 85)
        total = total - 85
        # Title / Author
        self.results_view.setColumnWidth(1, int(total * .40))
        # Price
        self.results_view.setColumnWidth(2, int(total * .12))
        # DRM
        self.results_view.setColumnWidth(3, int(total * .15))
        # Store / Formats
        self.results_view.setColumnWidth(4, int(total * .25))
        # Download
        self.results_view.setColumnWidth(5, 20)
        # Affiliate
        self.results_view.setColumnWidth(6, 20)

    def toggle_search(self):
        if self.searching:
            self.search_pool.abort()
            m = self.results_view.model()
            m.details_pool.abort()
            m.cover_pool.abort()
            self.search.setText(self.SEARCH_TEXT)
            self.checker.stop()
            self.searching = False
        else:
            self.do_search()
        # Prevent hitting the enter key twice in quick succession causing
        # the search to start and stop
        self.search.setEnabled(False)
        QTimer.singleShot(1000, lambda: self.search.setEnabled(True))

    def do_search(self):
        # Stop all running threads.
        self.checker.stop()
        self.search_pool.abort()
        # Clear the visible results.
        self.results_view.model().clear_results()

        # Don't start a search if there is nothing to search for.
        query = []
        if self.search_title.text():
            query.append('title2:"~%s"' %
                         str(self.search_title.text()).replace('"', ' '))
        if self.search_author.text():
            query.append('author2:"%s"' %
                         str(self.search_author.text()).replace('"', ' '))
        if self.search_edit.text():
            query.append(str(self.search_edit.text()))
        query = " ".join(query)
        if not query.strip():
            error_dialog(self,
                         _('No query'),
                         _('You must enter a title, author or keyword to'
                           ' search for.'),
                         show=True)
            return
        self.searching = True
        self.search.setText(self.STOP_TEXT)
        # Give the query to the results model so it can do
        # further filtering.
        self.results_view.model().set_query(query)

        # Plugins are in random order that does not change.
        # Randomize the ord of the plugin names every time
        # there is a search. This way plugins closer
        # to a don't have an unfair advantage over
        # plugins further from a.
        store_names = list(self.store_checks)
        if not store_names:
            return
        # Remove all of our internal filtering logic from the query.
        query = self.clean_query(query)
        shuffle(store_names)
        # Add plugins that the user has checked to the search pool's work queue.
        self.gui.istores.join(4.0)  # Wait for updated plugins to load
        for n in store_names:
            if self.store_checks[n].isChecked():
                self.search_pool.add_task(query, n, self.gui.istores[n],
                                          self.max_results, self.timeout)
        self.hang_check = 0
        self.checker.start(100)
        self.pi.startAnimation()

    def clean_query(self, query):
        query = query.lower()
        # Remove control modifiers.
        query = query.replace('\\', '')
        query = query.replace('!', '')
        query = query.replace('=', '')
        query = query.replace('~', '')
        query = query.replace('>', '')
        query = query.replace('<', '')
        # Remove the prefix.
        for loc in ('all', 'author', 'author2', 'authors', 'title', 'title2'):
            query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, r'\g<a>', query)
            query = query.replace('%s:' % loc, '')
        # Remove the prefix and search text.
        for loc in ('cover', 'download', 'downloads', 'drm', 'format',
                    'formats', 'price', 'store'):
            query = re.sub(r'%s:"[^"]"' % loc, '', query)
            query = re.sub(r'%s:[^\s]*' % loc, '', query)
        # Remove logic.
        query = re.sub(r'(^|\s|")(and|not|or|a|the|is|of)(\s|$|")', r' ',
                       query)
        # Remove "
        query = query.replace('"', '')
        # Remove excess whitespace.
        query = re.sub(r'\s+', ' ', query)
        query = query.strip()
        return query.encode('utf-8')

    def save_state(self):
        self.config['geometry'] = bytearray(self.saveGeometry())
        self.config['store_splitter_state'] = bytearray(
            self.store_splitter.saveState())
        self.config['results_view_column_width'] = [
            self.results_view.columnWidth(i)
            for i in range(self.results_view.model().columnCount())
        ]
        self.config['sort_col'] = self.results_view.model().sort_col
        self.config['sort_order'] = self.results_view.model().sort_order
        self.config['open_external'] = self.open_external.isChecked()

        store_check = {}
        for k, v in self.store_checks.items():
            store_check[k] = v.isChecked()
        self.config['store_checked'] = store_check

    def restore_state(self):
        geometry = self.config.get('geometry', None)
        if geometry:
            QApplication.instance().safe_restore_geometry(self, geometry)

        splitter_state = self.config.get('store_splitter_state', None)
        if splitter_state:
            self.store_splitter.restoreState(splitter_state)

        results_cwidth = self.config.get('results_view_column_width', None)
        if results_cwidth:
            for i, x in enumerate(results_cwidth):
                if i >= self.results_view.model().columnCount():
                    break
                self.results_view.setColumnWidth(i, x)
        else:
            self.resize_columns()

        self.open_external.setChecked(self.should_open_external)

        store_check = self.config.get('store_checked', None)
        if store_check:
            for n in store_check:
                if n in self.store_checks:
                    self.store_checks[n].setChecked(store_check[n])

        self.results_view.model().sort_col = self.config.get('sort_col', 2)
        self.results_view.model().sort_order = self.config.get(
            'sort_order', Qt.SortOrder.AscendingOrder)
        self.results_view.header().setSortIndicator(
            self.results_view.model().sort_col,
            self.results_view.model().sort_order)

    def load_settings(self):
        # Seconds
        self.timeout = self.config.get('timeout', 75)
        # Milliseconds
        self.hang_time = self.config.get('hang_time', 75) * 1000

        self.max_results = self.config.get('max_results', 15)
        self.should_open_external = self.config.get('open_external', True)

        # Number of threads to run for each type of operation
        self.search_thread_count = self.config.get('search_thread_count', 4)
        self.cache_thread_count = self.config.get('cache_thread_count', 2)
        self.cover_thread_count = self.config.get('cover_thread_count', 2)
        self.details_thread_count = self.config.get('details_thread_count', 4)

    def do_config(self):
        # Save values that need to be synced between the dialog and the
        # search widget.
        self.config['open_external'] = self.open_external.isChecked()

        # Create the config dialog. It's going to put two config widgets
        # into a QTabWidget for displaying all of the settings.
        d = QDialog(self)
        button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
        v = QVBoxLayout(d)
        button_box.accepted.connect(d.accept)
        button_box.rejected.connect(d.reject)
        d.setWindowTitle(_('Customize Get books search'))

        tab_widget = QTabWidget(d)
        v.addWidget(tab_widget)
        v.addWidget(button_box)

        chooser_config_widget = StoreChooserWidget()
        search_config_widget = StoreConfigWidget(self.config)

        tab_widget.addTab(chooser_config_widget, _('Choose s&tores'))
        tab_widget.addTab(search_config_widget, _('Configure s&earch'))

        # Restore dialog state.
        geometry = self.config.get('config_dialog_geometry', None)
        if geometry:
            QApplication.instance().safe_restore_geometry(d, geometry)
        else:
            d.resize(800, 600)
        tab_index = self.config.get('config_dialog_tab_index', 0)
        tab_index = min(tab_index, tab_widget.count() - 1)
        tab_widget.setCurrentIndex(tab_index)

        d.exec()

        # Save dialog state.
        self.config['config_dialog_geometry'] = bytearray(d.saveGeometry())
        self.config['config_dialog_tab_index'] = tab_widget.currentIndex()

        search_config_widget.save_settings()
        self.config_changed()
        self.gui.load_store_plugins()
        self.setup_store_checks()

    def config_changed(self):
        self.load_settings()

        self.open_external.setChecked(self.should_open_external)
        self.search_pool.set_thread_count(self.search_thread_count)
        self.cache_pool.set_thread_count(self.cache_thread_count)
        self.results_view.model().cover_pool.set_thread_count(
            self.cover_thread_count)
        self.results_view.model().details_pool.set_thread_count(
            self.details_thread_count)

    def get_results(self):
        # We only want the search plugins to run
        # a maximum set amount of time before giving up.
        self.hang_check += 1
        if self.hang_check >= self.hang_time:
            self.search_pool.abort()
            self.checker.stop()
        else:
            # Stop the checker if not threads are running.
            if not self.search_pool.threads_running(
            ) and not self.search_pool.has_tasks():
                self.checker.stop()

        while self.search_pool.has_results():
            res, store_plugin = self.search_pool.get_result()
            if res:
                self.results_view.model().add_result(res, store_plugin)

        if not self.search_pool.threads_running(
        ) and not self.results_view.model().has_results():
            info_dialog(self,
                        _('No matches'),
                        _('Couldn\'t find any books matching your query.'),
                        show=True,
                        show_copy_button=False)

    def update_book_total(self, total):
        self.total.setText('%s' % total)

    def result_item_activated(self, index):
        result = self.results_view.model().get_result(index)

        if result.downloads:
            self.download_book(result)
        else:
            self.open_store(result)

    def download_book(self, result):
        d = ChooseFormatDialog(self,
                               _('Choose format to download to your library.'),
                               list(result.downloads.keys()))
        if d.exec() == QDialog.DialogCode.Accepted:
            ext = d.format()
            fname = result.title[:60] + '.' + ext.lower()
            fname = ascii_filename(fname)
            show_download_info(result.title, parent=self)
            self.gui.download_ebook(result.downloads[ext],
                                    filename=fname,
                                    create_browser=result.create_browser)

    def open_store(self, result):
        self.gui.istores[result.store_name].open(
            self, result.detail_item, self.open_external.isChecked())

    def check_progress(self):
        m = self.results_view.model()
        if not self.search_pool.threads_running(
        ) and not m.cover_pool.threads_running(
        ) and not m.details_pool.threads_running():
            self.pi.stopAnimation()
            self.search.setText(self.SEARCH_TEXT)
            self.searching = False
        else:
            self.searching = True
            if str(self.search.text()) != self.STOP_TEXT:
                self.search.setText(self.STOP_TEXT)
            if not self.pi.isAnimated():
                self.pi.startAnimation()

    def stores_select_all(self):
        for check in self.store_checks.values():
            check.setChecked(True)

    def stores_select_invert(self):
        for check in self.store_checks.values():
            check.setChecked(not check.isChecked())

    def stores_select_none(self):
        for check in self.store_checks.values():
            check.setChecked(False)

    def dialog_closed(self, result):
        self.results_view.model().closing()
        self.search_pool.abort()
        self.cache_pool.abort()
        self.save_state()

    def exec(self):
        if str(self.search_edit.text()).strip() or str(
                self.search_title.text()).strip() or str(
                    self.search_author.text()).strip():
            self.do_search()
        return QDialog.exec(self)

    exec_ = exec
예제 #14
0
파일: jobs.py 프로젝트: kovidgoyal/calibre
class JobManager(QAbstractTableModel, AdaptSQP):  # {{{

    job_added = pyqtSignal(int)
    job_done  = pyqtSignal(int)

    def __init__(self):
        QAbstractTableModel.__init__(self)
        SearchQueryParser.__init__(self, ['all'])

        self.wait_icon     = (QIcon(I('jobs.png')))
        self.running_icon  = (QIcon(I('exec.png')))
        self.error_icon    = (QIcon(I('dialog_error.png')))
        self.done_icon     = (QIcon(I('ok.png')))

        self.jobs          = []
        self.add_job       = Dispatcher(self._add_job)
        self.server        = Server(limit=config['worker_limit']//2,
                                enforce_cpu_limit=config['enforce_cpu_limit'])
        self.threaded_server = ThreadedJobServer()
        self.changed_queue = Queue()

        self.timer         = QTimer(self)
        self.timer.timeout.connect(self.update, type=Qt.ConnectionType.QueuedConnection)
        self.timer.start(1000)

    def columnCount(self, parent=QModelIndex()):
        return 5

    def rowCount(self, parent=QModelIndex()):
        return len(self.jobs)

    def headerData(self, section, orientation, role):
        if role != Qt.ItemDataRole.DisplayRole:
            return None
        if orientation == Qt.Orientation.Horizontal:
            return ({
              0: _('Job'),
              1: _('Status'),
              2: _('Progress'),
              3: _('Running time'),
              4: _('Start time'),
            }.get(section, ''))
        else:
            return (section+1)

    def show_tooltip(self, arg):
        widget, pos = arg
        QToolTip.showText(pos, self.get_tooltip())

    def get_tooltip(self):
        running_jobs = [j for j in self.jobs if j.run_state == j.RUNNING]
        waiting_jobs = [j for j in self.jobs if j.run_state == j.WAITING]
        lines = [ngettext('There is a running job:', 'There are {} running jobs:', len(running_jobs)).format(len(running_jobs))]
        for job in running_jobs:
            desc = job.description
            if not desc:
                desc = _('Unknown job')
            p = 100. if job.is_finished else job.percent
            lines.append('%s:  %.0f%% done'%(desc, p))
        l = ngettext('There is a waiting job', 'There are {} waiting jobs', len(waiting_jobs)).format(len(waiting_jobs))
        lines.extend(['', l])
        for job in waiting_jobs:
            desc = job.description
            if not desc:
                desc = _('Unknown job')
            lines.append(desc)
        return '\n'.join(['calibre', '']+ lines)

    def data(self, index, role):
        try:
            if role not in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.DecorationRole):
                return None
            row, col = index.row(), index.column()
            job = self.jobs[row]

            if role == Qt.ItemDataRole.DisplayRole:
                if col == 0:
                    desc = job.description
                    if not desc:
                        desc = _('Unknown job')
                    return (desc)
                if col == 1:
                    return (job.status_text)
                if col == 2:
                    p = 100. if job.is_finished else job.percent
                    return (p)
                if col == 3:
                    rtime = job.running_time
                    if rtime is None:
                        return None
                    return human_readable_interval(rtime)
                if col == 4 and job.start_time is not None:
                    return (strftime('%H:%M -- %d %b', time.localtime(job.start_time)))
            if role == Qt.ItemDataRole.DecorationRole and col == 0:
                state = job.run_state
                if state == job.WAITING:
                    return self.wait_icon
                if state == job.RUNNING:
                    return self.running_icon
                if job.killed or job.failed:
                    return self.error_icon
                return self.done_icon
        except:
            import traceback
            traceback.print_exc()
        return None

    def update(self):
        try:
            self._update()
        except BaseException:
            import traceback
            traceback.print_exc()

    def _update(self):
        # Update running time
        for i, j in enumerate(self.jobs):
            if j.run_state == j.RUNNING:
                idx = self.index(i, 3)
                self.dataChanged.emit(idx, idx)

        # Update parallel jobs
        jobs = set()
        while True:
            try:
                jobs.add(self.server.changed_jobs_queue.get_nowait())
            except Empty:
                break

        # Update device jobs
        while True:
            try:
                jobs.add(self.changed_queue.get_nowait())
            except Empty:
                break

        # Update threaded jobs
        while True:
            try:
                jobs.add(self.threaded_server.changed_jobs.get_nowait())
            except Empty:
                break

        if jobs:
            needs_reset = False
            for job in jobs:
                orig_state = job.run_state
                job.update()
                if orig_state != job.run_state:
                    needs_reset = True
                    if job.is_finished:
                        self.job_done.emit(len(self.unfinished_jobs()))
            if needs_reset:
                self.modelAboutToBeReset.emit()
                self.jobs.sort()
                self.modelReset.emit()
            else:
                for job in jobs:
                    idx = self.jobs.index(job)
                    self.dataChanged.emit(
                        self.index(idx, 0), self.index(idx, 3))

        # Kill parallel jobs that have gone on too long
        try:
            wmax_time = gprefs['worker_max_time'] * 60
        except:
            wmax_time = 0

        if wmax_time > 0:
            for job in self.jobs:
                if isinstance(job, ParallelJob):
                    rtime = job.running_time
                    if (rtime is not None and rtime > wmax_time and
                            job.duration is None):
                        job.timed_out = True
                        self.server.kill_job(job)

    def _add_job(self, job):
        self.modelAboutToBeReset.emit()
        self.jobs.append(job)
        self.jobs.sort()
        self.job_added.emit(len(self.unfinished_jobs()))
        self.modelReset.emit()

    def done_jobs(self):
        return [j for j in self.jobs if j.is_finished]

    def unfinished_jobs(self):
        return [j for j in self.jobs if not j.is_finished]

    def row_to_job(self, row):
        return self.jobs[row]

    def rows_to_jobs(self, rows):
        return [self.jobs[row] for row in rows]

    def has_device_jobs(self, queued_also=False):
        for job in self.jobs:
            if isinstance(job, DeviceJob):
                if job.duration is None:  # Running or waiting
                    if (job.is_running or queued_also):
                        return True
        return False

    def has_jobs(self):
        for job in self.jobs:
            if job.is_running:
                return True
        return False

    def run_job(self, done, name, args=[], kwargs={},
                           description='', core_usage=1):
        job = ParallelJob(name, description, done, args=args, kwargs=kwargs)
        job.core_usage = core_usage
        self.add_job(job)
        self.server.add_job(job)
        return job

    def run_threaded_job(self, job):
        self.add_job(job)
        self.threaded_server.add_job(job)

    def launch_gui_app(self, name, args=(), kwargs=None, description=''):
        job = ParallelJob(name, description, lambda x: x,
                args=list(args), kwargs=kwargs or {})
        self.server.run_job(job, gui=True, redirect_output=False)

    def _kill_job(self, job):
        if isinstance(job, ParallelJob):
            self.server.kill_job(job)
        elif isinstance(job, ThreadedJob):
            self.threaded_server.kill_job(job)
        else:
            job.kill_on_start = True

    def hide_jobs(self, rows):
        for r in rows:
            self.jobs[r].hidden_in_gui = True
        for r in rows:
            self.dataChanged.emit(self.index(r, 0), self.index(r, 0))

    def show_hidden_jobs(self):
        for j in self.jobs:
            j.hidden_in_gui = False
        for r in range(len(self.jobs)):
            self.dataChanged.emit(self.index(r, 0), self.index(r, 0))

    def kill_job(self, job, view):
        if isinstance(job, DeviceJob):
            return error_dialog(view, _('Cannot kill job'),
                         _('Cannot kill jobs that communicate with the device')).exec()
        if job.duration is not None:
            return error_dialog(view, _('Cannot kill job'),
                         _('Job has already run')).exec()
        if not getattr(job, 'killable', True):
            return error_dialog(view, _('Cannot kill job'),
                    _('This job cannot be stopped'), show=True)
        self._kill_job(job)

    def kill_multiple_jobs(self, jobs, view):
        devjobs = [j for j in jobs if isinstance(j, DeviceJob)]
        if devjobs:
            error_dialog(view, _('Cannot kill job'),
                         _('Cannot kill jobs that communicate with the device')).exec()
            jobs = [j for j in jobs if not isinstance(j, DeviceJob)]
        jobs = [j for j in jobs if j.duration is None]
        unkillable = [j for j in jobs if not getattr(j, 'killable', True)]
        if unkillable:
            names = '\n'.join(as_unicode(j.description) for j in unkillable)
            error_dialog(view, _('Cannot kill job'),
                    _('Some of the jobs cannot be stopped. Click "Show details"'
                        ' to see the list of unstoppable jobs.'), det_msg=names,
                    show=True)
            jobs = [j for j in jobs if getattr(j, 'killable', True)]
        jobs = [j for j in jobs if j.duration is None]
        for j in jobs:
            self._kill_job(j)

    def kill_all_jobs(self):
        for job in self.jobs:
            if (isinstance(job, DeviceJob) or job.duration is not None or
                    not getattr(job, 'killable', True)):
                continue
            self._kill_job(job)

    def terminate_all_jobs(self):
        self.server.killall()
        for job in self.jobs:
            if (isinstance(job, DeviceJob) or job.duration is not None or
                    not getattr(job, 'killable', True)):
                continue
            if not isinstance(job, ParallelJob):
                self._kill_job(job)

    def universal_set(self):
        return {i for i, j in enumerate(self.jobs) if not getattr(j,
            'hidden_in_gui', False)}

    def get_matches(self, location, query, candidates=None):
        if candidates is None:
            candidates = self.universal_set()
        ans = set()
        if not query:
            return ans
        query = lower(query)
        for j in candidates:
            job = self.jobs[j]
            if job.description and query in lower(job.description):
                ans.add(j)
        return ans

    def find(self, query):
        query = query.strip()
        rows = self.parse(query)
        return rows