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)
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
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)
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)