Example #1
0
    def choices_widget(self,
                       name,
                       choices,
                       fallback_val,
                       none_val,
                       prefs=None):
        prefs = prefs or tprefs
        widget = QComboBox(self)
        widget.currentIndexChanged[int].connect(self.emit_changed)
        for key, human in sorted(
                iteritems(choices),
                key=lambda key_human: key_human[1] or key_human[0]):
            widget.addItem(human or key, key)

        def getter(w):
            ans = str(w.itemData(w.currentIndex()) or '')
            return {none_val: None}.get(ans, ans)

        def setter(w, val):
            val = {None: none_val}.get(val, val)
            idx = w.findData(val,
                             flags=Qt.MatchFlag.MatchFixedString
                             | Qt.MatchFlag.MatchCaseSensitive)
            if idx == -1:
                idx = w.findData(fallback_val,
                                 flags=Qt.MatchFlag.MatchFixedString
                                 | Qt.MatchFlag.MatchCaseSensitive)
            w.setCurrentIndex(idx)

        return self(name,
                    widget=widget,
                    getter=getter,
                    setter=setter,
                    prefs=prefs)
Example #2
0
 def make_color_combobox(self, row, dex):
     c = QComboBox(self)
     c.addItem('')
     c.addItems(QColor.colorNames())
     self.table.setCellWidget(row, 1, c)
     if dex >= 0:
         c.setCurrentIndex(dex)
     return c
Example #3
0
 def ins_button_clicked(self):
     row = self.table.currentRow()
     if row < 0:
         error_dialog(self, _('Select a cell'),
                            _('Select a cell before clicking the button'), show=True)
         return
     self.table.insertRow(row)
     self.table.setItem(row, 0, QTableWidgetItem())
     c = QComboBox(self)
     c.addItem('')
     c.addItems(QColor.colorNames())
     self.table.setCellWidget(row, 1, c)
class CreateVirtualLibrary(QDialog):  # {{{

    def __init__(self, gui, existing_names, editing=None):
        QDialog.__init__(self, gui)

        self.gui = gui
        self.existing_names = existing_names

        if editing:
            self.setWindowTitle(_('Edit Virtual library'))
        else:
            self.setWindowTitle(_('Create Virtual library'))
        self.setWindowIcon(QIcon(I('lt.png')))

        gl = QGridLayout()
        self.setLayout(gl)
        self.la1 = la1 = QLabel(_('Virtual library &name:'))
        gl.addWidget(la1, 0, 0)
        self.vl_name = QComboBox()
        self.vl_name.setEditable(True)
        self.vl_name.lineEdit().setMaxLength(MAX_VIRTUAL_LIBRARY_NAME_LENGTH)
        la1.setBuddy(self.vl_name)
        gl.addWidget(self.vl_name, 0, 1)
        self.editing = editing

        self.saved_searches_label = sl = QTextBrowser(self)
        sl.viewport().setAutoFillBackground(False)
        gl.addWidget(sl, 2, 0, 1, 2)

        self.la2 = la2 = QLabel(_('&Search expression:'))
        gl.addWidget(la2, 1, 0)
        self.vl_text = QLineEdit()
        self.vl_text.textChanged.connect(self.search_text_changed)
        la2.setBuddy(self.vl_text)
        gl.addWidget(self.vl_text, 1, 1)
        # Trigger the textChanged signal to initialize the saved searches box
        self.vl_text.setText(' ')
        self.vl_text.setText(_build_full_search_string(self.gui))

        self.sl = sl = QLabel('<p>'+_('Create a Virtual library based on: ')+
            ('<a href="author.{0}">{0}</a>, '
            '<a href="tag.{1}">{1}</a>, '
            '<a href="publisher.{2}">{2}</a>, '
            '<a href="series.{3}">{3}</a>, '
            '<a href="search.{4}">{4}</a>.').format(_('Authors'), _('Tags'),
                                            _('Publishers'), ngettext('Series', 'Series', 2), _('Saved searches')))
        sl.setWordWrap(True)
        sl.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)
        sl.linkActivated.connect(self.link_activated)
        gl.addWidget(sl, 3, 0, 1, 2)
        gl.setRowStretch(3,10)

        self.hl = hl = QLabel(_('''
            <h2>Virtual libraries</h2>

            <p>With <i>Virtual libraries</i>, you can restrict calibre to only show
            you books that match a search. When a Virtual library is in effect, calibre
            behaves as though the library contains only the matched books. The Tag browser
            display only the tags/authors/series/etc. that belong to the matched books and any searches
            you do will only search within the books in the Virtual library. This
            is a good way to partition your large library into smaller and easier to work with subsets.</p>

            <p>For example you can use a Virtual library to only show you books with the tag <i>Unread</i>
            or only books by <i>My favorite author</i> or only books in a particular series.</p>

            <p>More information and examples are available in the
            <a href="%s">User Manual</a>.</p>
            ''') % localize_user_manual_link('https://manual.calibre-ebook.com/virtual_libraries.html'))
        hl.setWordWrap(True)
        hl.setOpenExternalLinks(True)
        hl.setFrameStyle(QFrame.Shape.StyledPanel)
        gl.addWidget(hl, 0, 3, 4, 1)

        bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        gl.addWidget(bb, 4, 0, 1, 0)

        if editing:
            db = self.gui.current_db
            virt_libs = db.new_api.pref('virtual_libraries', {})
            for dex,vl in enumerate(sorted(virt_libs.keys(), key=sort_key)):
                self.vl_name.addItem(vl, virt_libs.get(vl, ''))
                if vl == editing:
                    self.vl_name.setCurrentIndex(dex)
                    self.original_index = dex
            self.original_search = virt_libs.get(editing, '')
            self.vl_text.setText(self.original_search)
            self.new_name = editing
            self.vl_name.currentIndexChanged[int].connect(self.name_index_changed)
            self.vl_name.lineEdit().textEdited.connect(self.name_text_edited)

        self.resize(self.sizeHint()+QSize(150, 25))

    def search_text_changed(self, txt):
        db = self.gui.current_db
        searches = [_('Saved searches recognized in the expression:')]
        txt = str(txt)
        while txt:
            p = txt.partition('search:')
            if p[1]:  # found 'search:'
                possible_search = p[2]
                if possible_search:  # something follows the 'search:'
                    if possible_search[0] == '"':  # strip any quotes
                        possible_search = possible_search[1:].partition('"')
                    else:  # find end of the search name. Is EOL, space, rparen
                        sp = possible_search.find(' ')
                        pp = possible_search.find(')')
                        if pp < 0 or (sp > 0 and sp <= pp):
                            # space in string before rparen, or neither found
                            possible_search = possible_search.partition(' ')
                        else:
                            # rparen in string before space
                            possible_search = possible_search.partition(')')
                    txt = possible_search[2]  # grab remainder of the string
                    search_name = possible_search[0]
                    if search_name.startswith('='):
                        search_name = search_name[1:]
                    if search_name in db.saved_search_names():
                        searches.append(search_name + '=' +
                                        db.saved_search_lookup(search_name))
                else:
                    txt = ''
            else:
                txt = ''
        self.saved_searches_label.setPlainText('\n'.join(searches))

    def name_text_edited(self, new_name):
        self.new_name = str(new_name)

    def name_index_changed(self, dex):
        if self.editing and (self.vl_text.text() != self.original_search or
                             self.new_name != self.editing):
            if not question_dialog(self.gui, _('Search text changed'),
                         _('The Virtual library name or the search text has changed. '
                           'Do you want to discard these changes?'),
                         default_yes=False):
                self.vl_name.blockSignals(True)
                self.vl_name.setCurrentIndex(self.original_index)
                self.vl_name.lineEdit().setText(self.new_name)
                self.vl_name.blockSignals(False)
                return
        self.new_name = self.editing = self.vl_name.currentText()
        self.original_index = dex
        self.original_search = str(self.vl_name.itemData(dex) or '')
        self.vl_text.setText(self.original_search)

    def link_activated(self, url):
        db = self.gui.current_db
        f, txt = str(url).partition('.')[0::2]
        if f == 'search':
            names = db.saved_search_names()
        else:
            names = getattr(db, 'all_%s_names'%f)()
        d = SelectNames(names, txt, parent=self)
        if d.exec() == QDialog.DialogCode.Accepted:
            prefix = f+'s' if f in {'tag', 'author'} else f
            if f == 'search':
                search = ['(%s)'%(db.saved_search_lookup(x)) for x in d.names]
            else:
                search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names]
            if search:
                if not self.editing:
                    self.vl_name.lineEdit().setText(next(d.names))
                    self.vl_name.lineEdit().setCursorPosition(0)
                self.vl_text.setText(d.match_type.join(search))
                self.vl_text.setCursorPosition(0)

    def accept(self):
        n = str(self.vl_name.currentText()).strip()
        if not n:
            error_dialog(self.gui, _('No name'),
                         _('You must provide a name for the new Virtual library'),
                         show=True)
            return

        if n.startswith('*'):
            error_dialog(self.gui, _('Invalid name'),
                         _('A Virtual library name cannot begin with "*"'),
                         show=True)
            return

        if n in self.existing_names and n != self.editing:
            if not question_dialog(self.gui, _('Name already in use'),
                         _('That name is already in use. Do you want to replace it '
                           'with the new search?'),
                            default_yes=False):
                return

        v = str(self.vl_text.text()).strip()
        if not v:
            error_dialog(self.gui, _('No search string'),
                         _('You must provide a search to define the new Virtual library'),
                         show=True)
            return

        try:
            db = self.gui.library_view.model().db
            recs = db.data.search_getting_ids('', v, use_virtual_library=False, sort_results=False)
        except ParseException as e:
            error_dialog(self.gui, _('Invalid search'),
                         _('The search in the search box is not valid'),
                         det_msg=e.msg, show=True)
            return

        if not recs and not question_dialog(
                self.gui, _('Search found no books'),
                _('The search found no books, so the Virtual library '
                'will be empty. Do you really want to use that search?'),
                default_yes=False):
            return

        self.library_name = n
        self.library_search = v
        QDialog.accept(self)
Example #5
0
class InsertSemantics(Dialog):

    def __init__(self, container, parent=None):
        self.container = container
        self.create_known_type_map()
        self.anchor_cache = {}
        self.original_guide_map = {item['type']: item for item in get_guide_landmarks(container)}
        self.original_nav_map = {item['type']: item for item in get_nav_landmarks(container)}
        self.changes = {}
        Dialog.__init__(self, _('Set semantics'), 'insert-semantics', parent=parent)

    def sizeHint(self):
        return QSize(800, 600)

    def create_known_type_map(self):
        _ = lambda x: x
        self.epubtype_guide_map = {v: k for k, v in guide_epubtype_map.items()}
        self.known_type_map = {
            'titlepage': _('Title page'),
            'toc': _('Table of Contents'),
            'index': _('Index'),
            'glossary': _('Glossary'),
            'acknowledgments': _('Acknowledgements'),
            'bibliography': _('Bibliography'),
            'colophon': _('Colophon'),
            'cover': _('Cover'),
            'copyright-page': _('Copyright page'),
            'dedication': _('Dedication'),
            'epigraph': _('Epigraph'),
            'foreword': _('Foreword'),
            'loi': _('List of illustrations'),
            'lot': _('List of tables'),
            'notes': _('Notes'),
            'preface': _('Preface'),
            'bodymatter': _('Text'),
        }
        _ = __builtins__['_']
        type_map_help = {
            'titlepage': _('Page with title, author, publisher, etc.'),
            'cover': _('The book cover, typically a single HTML file with a cover image inside'),
            'index': _('Back-of-book style index'),
            'bodymatter': _('First "real" page of content'),
        }
        t = _
        all_types = [(k, (('%s (%s)' % (t(v), type_map_help[k])) if k in type_map_help else t(v))) for k, v in iteritems(self.known_type_map)]
        all_types.sort(key=lambda x: sort_key(x[1]))
        self.all_types = OrderedDict(all_types)

    def setup_ui(self):
        self.l = l = QVBoxLayout(self)
        self.setLayout(l)

        self.tl = tl = QFormLayout()
        tl.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
        self.semantic_type = QComboBox(self)
        for key, val in iteritems(self.all_types):
            self.semantic_type.addItem(val, key)
        tl.addRow(_('Type of &semantics:'), self.semantic_type)
        self.target = t = QLineEdit(self)
        t.setClearButtonEnabled(True)
        t.setPlaceholderText(_('The destination (href) for the link'))
        tl.addRow(_('&Target:'), t)
        l.addLayout(tl)

        self.hline = hl = QFrame(self)
        hl.setFrameStyle(QFrame.Shape.HLine)
        l.addWidget(hl)

        self.h = h = QHBoxLayout()
        l.addLayout(h)

        names = [n for n, linear in self.container.spine_names]
        fn, f = create_filterable_names_list(names, filter_text=_('Filter files'), parent=self)
        self.file_names, self.file_names_filter = fn, f
        fn.selectionModel().selectionChanged.connect(self.selected_file_changed)
        self.fnl = fnl = QVBoxLayout()
        self.la1 = la = QLabel(_('Choose a &file:'))
        la.setBuddy(fn)
        fnl.addWidget(la), fnl.addWidget(f), fnl.addWidget(fn)
        h.addLayout(fnl), h.setStretch(0, 2)

        fn, f = create_filterable_names_list([], filter_text=_('Filter locations'), parent=self)
        self.anchor_names, self.anchor_names_filter = fn, f
        fn.selectionModel().selectionChanged.connect(self.update_target)
        fn.doubleClicked.connect(self.accept, type=Qt.ConnectionType.QueuedConnection)
        self.anl = fnl = QVBoxLayout()
        self.la2 = la = QLabel(_('Choose a &location (anchor) in the file:'))
        la.setBuddy(fn)
        fnl.addWidget(la), fnl.addWidget(f), fnl.addWidget(fn)
        h.addLayout(fnl), h.setStretch(1, 1)

        self.bb.addButton(QDialogButtonBox.StandardButton.Help)
        self.bb.helpRequested.connect(self.help_requested)
        l.addWidget(self.bb)
        self.semantic_type_changed()
        self.semantic_type.currentIndexChanged.connect(self.semantic_type_changed)
        self.target.textChanged.connect(self.target_text_changed)

    def help_requested(self):
        d = info_dialog(self, _('About semantics'), _(
            'Semantics refer to additional information about specific locations in the book.'
            ' For example, you can specify that a particular location is the dedication or the preface'
            ' or the Table of Contents and so on.\n\nFirst choose the type of semantic information, then'
            ' choose a file and optionally a location within the file to point to.\n\nThe'
            ' semantic information will be written in the <guide> section of the OPF file.'))
        d.resize(d.sizeHint())
        d.exec()

    def dest_for_type(self, item_type):
        if item_type in self.changes:
            return self.changes[item_type]
        if item_type in self.original_nav_map:
            item = self.original_nav_map[item_type]
            return item['dest'], item['frag']
        item_type = self.epubtype_guide_map.get(item_type, item_type)
        if item_type in self.original_guide_map:
            item = self.original_guide_map[item_type]
            return item['dest'], item['frag']
        return None, None

    def semantic_type_changed(self):
        item_type = str(self.semantic_type.itemData(self.semantic_type.currentIndex()) or '')
        name, frag = self.dest_for_type(item_type)
        self.show_type(name, frag)

    def show_type(self, name, frag):
        self.file_names_filter.clear(), self.anchor_names_filter.clear()
        self.file_names.clearSelection(), self.anchor_names.clearSelection()
        if name is not None:
            row = self.file_names.model().find_name(name)
            if row is not None:
                sm = self.file_names.selectionModel()
                sm.select(self.file_names.model().index(row), QItemSelectionModel.SelectionFlag.ClearAndSelect)
                if frag:
                    row = self.anchor_names.model().find_name(frag)
                    if row is not None:
                        sm = self.anchor_names.selectionModel()
                        sm.select(self.anchor_names.model().index(row), QItemSelectionModel.SelectionFlag.ClearAndSelect)
        self.target.blockSignals(True)
        if name is not None:
            self.target.setText(name + (('#' + frag) if frag else ''))
        else:
            self.target.setText('')
        self.target.blockSignals(False)

    def target_text_changed(self):
        name, frag = str(self.target.text()).partition('#')[::2]
        item_type = str(self.semantic_type.itemData(self.semantic_type.currentIndex()) or '')
        if item_type:
            self.changes[item_type] = (name, frag or None)

    def selected_file_changed(self, *args):
        rows = list(self.file_names.selectionModel().selectedRows())
        if not rows:
            self.anchor_names.model().set_names([])
        else:
            name, positions = self.file_names.model().data(rows[0], Qt.ItemDataRole.UserRole)
            self.populate_anchors(name)

    def populate_anchors(self, name):
        if name not in self.anchor_cache:
            from calibre.ebooks.oeb.base import XHTML_NS
            root = self.container.parsed(name)
            self.anchor_cache[name] = sorted(
                (set(root.xpath('//*/@id')) | set(root.xpath('//h:a/@name', namespaces={'h':XHTML_NS}))) - {''}, key=primary_sort_key)
        self.anchor_names.model().set_names(self.anchor_cache[name])
        self.update_target()

    def update_target(self):
        rows = list(self.file_names.selectionModel().selectedRows())
        if not rows:
            return
        name = self.file_names.model().data(rows[0], Qt.ItemDataRole.UserRole)[0]
        href = name
        frag = ''
        rows = list(self.anchor_names.selectionModel().selectedRows())
        if rows:
            anchor = self.anchor_names.model().data(rows[0], Qt.ItemDataRole.UserRole)[0]
            if anchor:
                frag = '#' + anchor
        href += frag
        self.target.setText(href or '#')

    def apply_changes(self, container):
        from calibre.ebooks.oeb.polish.opf import get_book_language, set_guide_item
        from calibre.translations.dynamic import translate
        lang = get_book_language(container)

        def title_for_type(item_type):
            title = self.known_type_map.get(item_type, item_type)
            if lang:
                title = translate(lang, title)
            return title

        for item_type, (name, frag) in self.changes.items():
            set_guide_item(container, self.epubtype_guide_map[item_type], title_for_type(item_type), name, frag=frag)

        if container.opf_version_parsed.major > 2:
            final = self.original_nav_map.copy()
            for item_type, (name, frag) in self.changes.items():
                final[item_type] = {'dest': name, 'frag': frag or '', 'title': title_for_type(item_type), 'type': item_type}
            tocname, root = ensure_container_has_nav(container, lang=lang)
            set_landmarks(container, root, tocname, final.values())
            container.dirty(tocname)

    @classmethod
    def test(cls):
        import sys

        from calibre.ebooks.oeb.polish.container import get_container
        c = get_container(sys.argv[-1], tweak_mode=True)
        d = cls(c)
        if d.exec() == QDialog.DialogCode.Accepted:
            import pprint
            pprint.pprint(d.changed_type_map)
            d.apply_changes(d.container)