class ItemEdit(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self.setLayout(QVBoxLayout()) self.la = la = QLabel('<b>'+_( 'Select a destination for the Table of Contents entry')) self.layout().addWidget(la) self.splitter = sp = QSplitter(self) self.layout().addWidget(sp) self.layout().setStretch(1, 10) sp.setOpaqueResize(False) sp.setChildrenCollapsible(False) self.dest_list = dl = QListWidget(self) dl.setMinimumWidth(250) dl.currentItemChanged.connect(self.current_changed) sp.addWidget(dl) w = self.w = QWidget(self) l = w.l = QGridLayout() w.setLayout(l) self.view = WebView(self) self.view.elem_clicked.connect(self.elem_clicked) l.addWidget(self.view, 0, 0, 1, 3) sp.addWidget(w) self.search_text = s = QLineEdit(self) s.setPlaceholderText(_('Search for text...')) l.addWidget(s, 1, 0) self.ns_button = b = QPushButton(QIcon(I('arrow-down.png')), _('Find &next'), self) b.clicked.connect(self.find_next) l.addWidget(b, 1, 1) self.ps_button = b = QPushButton(QIcon(I('arrow-up.png')), _('Find &previous'), self) l.addWidget(b, 1, 2) b.clicked.connect(self.find_previous) self.f = f = QFrame() f.setFrameShape(f.StyledPanel) f.setMinimumWidth(250) l = f.l = QVBoxLayout() f.setLayout(l) sp.addWidget(f) f.la = la = QLabel('<p>'+_( 'Here you can choose a destination for the Table of Contents\' entry' ' to point to. First choose a file from the book in the left-most panel. The' ' file will open in the central panel.<p>' 'Then choose a location inside the file. To do so, simply click on' ' the place in the central panel that you want to use as the' ' destination. As you move the mouse around the central panel, a' ' thick green line appears, indicating the precise location' ' that will be selected when you click.')) la.setStyleSheet('QLabel { margin-bottom: 20px }') la.setWordWrap(True) l.addWidget(la) f.la2 = la = QLabel('<b>'+_('&Name of the ToC entry:')) l.addWidget(la) self.name = QLineEdit(self) la.setBuddy(self.name) l.addWidget(self.name) self.base_msg = '<b>'+_('Currently selected destination:')+'</b>' self.dest_label = la = QLabel(self.base_msg) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-top: 20px }') l.addWidget(la) l.addStretch() state = gprefs.get('toc_edit_splitter_state', None) if state is not None: sp.restoreState(state) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Return, Qt.Key_Enter) and self.search_text.hasFocus(): # Prevent pressing enter in the search box from triggering the dialog's accept() method ev.accept() return return super(ItemEdit, self).keyPressEvent(ev) def find(self, forwards=True): text = unicode(self.search_text.text()).strip() flags = QWebPage.FindFlags(0) if forwards else QWebPage.FindBackward d = self.dest_list if d.count() == 1: flags |= QWebPage.FindWrapsAroundDocument if not self.view.findText(text, flags) and text: if d.count() == 1: return error_dialog(self, _('No match found'), _('No match found for: %s')%text, show=True) delta = 1 if forwards else -1 current = unicode(d.currentItem().data(Qt.DisplayRole).toString()) next_index = (d.currentRow() + delta)%d.count() next = unicode(d.item(next_index).data(Qt.DisplayRole).toString()) msg = '<p>'+_('No matches for %(text)s found in the current file [%(current)s].' ' Do you want to search in the %(which)s file [%(next)s]?') msg = msg%dict(text=text, current=current, next=next, which=_('next') if forwards else _('previous')) if question_dialog(self, _('No match found'), msg): self.pending_search = self.find_next if forwards else self.find_previous d.setCurrentRow(next_index) def find_next(self): return self.find() def find_previous(self): return self.find(forwards=False) def load(self, container): self.container = container spine_names = [container.abspath_to_name(p) for p in container.spine_items] spine_names = [n for n in spine_names if container.has_name(n)] self.dest_list.addItems(spine_names) def current_changed(self, item): name = self.current_name = unicode(item.data(Qt.DisplayRole).toString()) path = self.container.name_to_abspath(name) # Ensure encoding map is populated root = self.container.parsed(name) nasty = root.xpath('//*[local-name()="head"]/*[local-name()="p"]') if nasty: body = root.xpath('//*[local-name()="body"]') if not body: return error_dialog(self, _('Bad markup'), _('This book has severely broken markup, its ToC cannot be edited.'), show=True) for x in reversed(nasty): body[0].insert(0, x) self.container.commit_item(name, keep_parsed=True) encoding = self.container.encoding_map.get(name, None) or 'utf-8' load_html(path, self.view, codec=encoding, mime_type=self.container.mime_map[name]) self.view.load_js() self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + name + '<br>' + _('Top of the file')) if hasattr(self, 'pending_search'): f = self.pending_search del self.pending_search f() def __call__(self, item, where): self.current_item, self.current_where = item, where self.current_name = None self.current_frag = None self.name.setText(_('(Untitled)')) dest_index, frag = 0, None if item is not None: if where is None: self.name.setText(item.data(0, Qt.DisplayRole).toString()) self.name.setCursorPosition(0) toc = item.data(0, Qt.UserRole).toPyObject() if toc.dest: for i in xrange(self.dest_list.count()): litem = self.dest_list.item(i) if unicode(litem.data(Qt.DisplayRole).toString()) == toc.dest: dest_index = i frag = toc.frag break self.dest_list.blockSignals(True) self.dest_list.setCurrentRow(dest_index) self.dest_list.blockSignals(False) item = self.dest_list.item(dest_index) self.current_changed(item) if frag: self.current_frag = frag QTimer.singleShot(1, self.show_frag) def show_frag(self): self.view.show_frag(self.current_frag) QTimer.singleShot(1, self.check_frag) def check_frag(self): pos = self.view.scroll_frac if pos == 0: self.current_frag = None self.update_dest_label() def get_loctext(self, frac): frac = int(round(frac * 100)) if frac == 0: loctext = _('Top of the file') else: loctext = _('Approximately %d%% from the top')%frac return loctext def elem_clicked(self, tag, frac, elem_id, loc, totals): self.current_frag = elem_id or (loc, totals) base = _('Location: A <%s> tag inside the file')%tag loctext = base + ' [%s]'%self.get_loctext(frac) self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + self.current_name + '<br>' + loctext) def update_dest_label(self): val = self.view.scroll_frac self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + self.current_name + '<br>' + self.get_loctext(val)) @property def result(self): return (self.current_item, self.current_where, self.current_name, self.current_frag, unicode(self.name.text()))
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 = QLabel('') self.saved_searches_label.setTextInteractionFlags(Qt.TextSelectableByMouse) gl.addWidget(self.saved_searches_label, 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) 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'), _('Series'), _('Saved Searches'))) sl.setWordWrap(True) sl.setTextInteractionFlags(Qt.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>Using <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="http://manual.calibre-ebook.com/virtual_libraries.html">User Manual</a>.</p> ''')) hl.setWordWrap(True) hl.setOpenExternalLinks(True) hl.setFrameStyle(hl.StyledPanel) gl.addWidget(hl, 0, 3, 4, 1) bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.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.prefs.get('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 = unicode(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 = '' if len(searches) > 1: self.saved_searches_label.setText('\n'.join(searches)) else: self.saved_searches_label.setText('') def name_text_edited(self, new_name): self.new_name = unicode(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 = unicode(self.vl_name.itemData(dex).toString()) self.vl_text.setText(self.original_search) def link_activated(self, url): db = self.gui.current_db f, txt = unicode(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_() == d.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(d.names.next()) self.vl_name.lineEdit().setCursorPosition(0) self.vl_text.setText(d.match_type.join(search)) self.vl_text.setCursorPosition(0) def accept(self): n = unicode(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 = unicode(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) 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 ConfigWidget(QWidget, Ui_ConfigWidget): def __init__(self, settings, all_formats, supports_subdirs, must_read_metadata, supports_use_author_sort, extra_customization_message, device, extra_customization_choices=None): QWidget.__init__(self) Ui_ConfigWidget.__init__(self) self.setupUi(self) self.settings = settings all_formats = set(all_formats) self.calibre_known_formats = device.FORMATS try: self.device_name = device.get_gui_name() except TypeError: self.device_name = getattr(device, 'gui_name', None) or _('Device') if device.USER_CAN_ADD_NEW_FORMATS: all_formats = set(all_formats) | set(BOOK_EXTENSIONS) format_map = settings.format_map disabled_formats = list(set(all_formats).difference(format_map)) for format in format_map + list(sorted(disabled_formats)): item = QListWidgetItem(format, self.columns) item.setData(Qt.UserRole, QVariant(format)) item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) item.setCheckState(Qt.Checked if format in format_map else Qt.Unchecked) self.column_up.clicked.connect(self.up_column) self.column_down.clicked.connect(self.down_column) if device.HIDE_FORMATS_CONFIG_BOX: self.groupBox.hide() if supports_subdirs: self.opt_use_subdirs.setChecked(self.settings.use_subdirs) else: self.opt_use_subdirs.hide() if not must_read_metadata: self.opt_read_metadata.setChecked(self.settings.read_metadata) else: self.opt_read_metadata.hide() if supports_use_author_sort: self.opt_use_author_sort.setChecked(self.settings.use_author_sort) else: self.opt_use_author_sort.hide() if extra_customization_message: extra_customization_choices = extra_customization_choices or {} def parse_msg(m): msg, _, tt = m.partition(':::') if m else ('', '', '') return msg.strip(), textwrap.fill(tt.strip(), 100) if isinstance(extra_customization_message, list): self.opt_extra_customization = [] if len(extra_customization_message) > 6: row_func = lambda x, y: ((x/2) * 2) + y col_func = lambda x: x%2 else: row_func = lambda x, y: x*2 + y col_func = lambda x: 0 for i, m in enumerate(extra_customization_message): label_text, tt = parse_msg(m) if not label_text: self.opt_extra_customization.append(None) continue if isinstance(settings.extra_customization[i], bool): self.opt_extra_customization.append(QCheckBox(label_text)) self.opt_extra_customization[-1].setToolTip(tt) self.opt_extra_customization[i].setChecked(bool(settings.extra_customization[i])) elif i in extra_customization_choices: cb = QComboBox(self) self.opt_extra_customization.append(cb) l = QLabel(label_text) l.setToolTip(tt), cb.setToolTip(tt), l.setBuddy(cb), cb.setToolTip(tt) for li in sorted(extra_customization_choices[i]): self.opt_extra_customization[i].addItem(li) cb.setCurrentIndex(max(0, cb.findText(settings.extra_customization[i]))) else: self.opt_extra_customization.append(QLineEdit(self)) l = QLabel(label_text) l.setToolTip(tt) self.opt_extra_customization[i].setToolTip(tt) l.setBuddy(self.opt_extra_customization[i]) l.setWordWrap(True) self.opt_extra_customization[i].setText(settings.extra_customization[i]) self.opt_extra_customization[i].setCursorPosition(0) self.extra_layout.addWidget(l, row_func(i, 0), col_func(i)) self.extra_layout.addWidget(self.opt_extra_customization[i], row_func(i, 1), col_func(i)) else: self.opt_extra_customization = QLineEdit() label_text, tt = parse_msg(extra_customization_message) l = QLabel(label_text) l.setToolTip(tt) l.setBuddy(self.opt_extra_customization) l.setWordWrap(True) if settings.extra_customization: self.opt_extra_customization.setText(settings.extra_customization) self.opt_extra_customization.setCursorPosition(0) self.opt_extra_customization.setCursorPosition(0) self.extra_layout.addWidget(l, 0, 0) self.extra_layout.addWidget(self.opt_extra_customization, 1, 0) self.opt_save_template.setText(settings.save_template) def up_column(self): idx = self.columns.currentRow() if idx > 0: self.columns.insertItem(idx-1, self.columns.takeItem(idx)) self.columns.setCurrentRow(idx-1) def down_column(self): idx = self.columns.currentRow() if idx < self.columns.count()-1: self.columns.insertItem(idx+1, self.columns.takeItem(idx)) self.columns.setCurrentRow(idx+1) def format_map(self): formats = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked] return formats def use_subdirs(self): return self.opt_use_subdirs.isChecked() def read_metadata(self): return self.opt_read_metadata.isChecked() def use_author_sort(self): return self.opt_use_author_sort.isChecked() def validate(self): formats = set(self.format_map()) extra = formats - set(self.calibre_known_formats) if extra: fmts = sorted([x.upper() for x in extra]) if not question_dialog(self, _('Unknown formats'), _('You have enabled the <b>{0}</b> formats for' ' your {1}. The {1} may not support them.' ' If you send these formats to your {1} they ' 'may not work. Are you sure?').format( (', '.join(fmts)), self.device_name)): return False tmpl = unicode(self.opt_save_template.text()) try: validation_formatter.validate(tmpl) return True except Exception as err: error_dialog(self, _('Invalid template'), '<p>'+_('The template %s is invalid:')%tmpl + '<br>'+unicode(err), show=True) return False
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 = QLabel('') self.saved_searches_label.setTextInteractionFlags( Qt.TextSelectableByMouse) gl.addWidget(self.saved_searches_label, 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) 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'), _('Series'), _('Saved Searches'))) sl.setWordWrap(True) sl.setTextInteractionFlags(Qt.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>Using <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="http://manual.calibre-ebook.com/virtual_libraries.html">User Manual</a>.</p> ''')) hl.setWordWrap(True) hl.setOpenExternalLinks(True) hl.setFrameStyle(hl.StyledPanel) gl.addWidget(hl, 0, 3, 4, 1) bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.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.prefs.get('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): searches = [_('Saved searches recognized in the expression:')] txt = unicode(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 saved_searches().names(): searches.append(search_name + '=' + saved_searches().lookup(search_name)) else: txt = '' else: txt = '' if len(searches) > 1: self.saved_searches_label.setText('\n'.join(searches)) else: self.saved_searches_label.setText('') def name_text_edited(self, new_name): self.new_name = unicode(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 = unicode(self.vl_name.itemData(dex).toString()) self.vl_text.setText(self.original_search) def link_activated(self, url): db = self.gui.current_db f, txt = unicode(url).partition('.')[0::2] if f == 'search': names = saved_searches().names() else: names = getattr(db, 'all_%s_names' % f)() d = SelectNames(names, txt, parent=self) if d.exec_() == d.Accepted: prefix = f + 's' if f in {'tag', 'author'} else f if f == 'search': search = [ '(%s)' % (saved_searches().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(d.names.next()) self.vl_name.lineEdit().setCursorPosition(0) self.vl_text.setText(d.match_type.join(search)) self.vl_text.setCursorPosition(0) def accept(self): n = unicode(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 = unicode(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) 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 ItemEdit(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self.setLayout(QVBoxLayout()) self.la = la = QLabel( '<b>' + _('Select a destination for the Table of Contents entry')) self.layout().addWidget(la) self.splitter = sp = QSplitter(self) self.layout().addWidget(sp) self.layout().setStretch(1, 10) sp.setOpaqueResize(False) sp.setChildrenCollapsible(False) self.dest_list = dl = QListWidget(self) dl.setMinimumWidth(250) dl.currentItemChanged.connect(self.current_changed) sp.addWidget(dl) w = self.w = QWidget(self) l = w.l = QGridLayout() w.setLayout(l) self.view = WebView(self) self.view.elem_clicked.connect(self.elem_clicked) l.addWidget(self.view, 0, 0, 1, 3) sp.addWidget(w) self.search_text = s = QLineEdit(self) s.setPlaceholderText(_('Search for text...')) l.addWidget(s, 1, 0) self.ns_button = b = QPushButton(QIcon(I('arrow-down.png')), _('Find &next'), self) b.clicked.connect(self.find_next) l.addWidget(b, 1, 1) self.ps_button = b = QPushButton(QIcon(I('arrow-up.png')), _('Find &previous'), self) l.addWidget(b, 1, 2) b.clicked.connect(self.find_previous) self.f = f = QFrame() f.setFrameShape(f.StyledPanel) f.setMinimumWidth(250) l = f.l = QVBoxLayout() f.setLayout(l) sp.addWidget(f) f.la = la = QLabel('<p>' + _( 'Here you can choose a destination for the Table of Contents\' entry' ' to point to. First choose a file from the book in the left-most panel. The' ' file will open in the central panel.<p>' 'Then choose a location inside the file. To do so, simply click on' ' the place in the central panel that you want to use as the' ' destination. As you move the mouse around the central panel, a' ' thick green line appears, indicating the precise location' ' that will be selected when you click.')) la.setStyleSheet('QLabel { margin-bottom: 20px }') la.setWordWrap(True) l.addWidget(la) f.la2 = la = QLabel('<b>' + _('&Name of the ToC entry:')) l.addWidget(la) self.name = QLineEdit(self) la.setBuddy(self.name) l.addWidget(self.name) self.base_msg = '<b>' + _('Currently selected destination:') + '</b>' self.dest_label = la = QLabel(self.base_msg) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-top: 20px }') l.addWidget(la) l.addStretch() state = gprefs.get('toc_edit_splitter_state', None) if state is not None: sp.restoreState(state) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Return, Qt.Key_Enter) and self.search_text.hasFocus(): # Prevent pressing enter in the search box from triggering the dialog's accept() method ev.accept() return return super(ItemEdit, self).keyPressEvent(ev) def find(self, forwards=True): text = unicode(self.search_text.text()).strip() flags = QWebPage.FindFlags(0) if forwards else QWebPage.FindBackward d = self.dest_list if d.count() == 1: flags |= QWebPage.FindWrapsAroundDocument if not self.view.findText(text, flags) and text: if d.count() == 1: return error_dialog(self, _('No match found'), _('No match found for: %s') % text, show=True) delta = 1 if forwards else -1 current = unicode(d.currentItem().data(Qt.DisplayRole).toString()) next_index = (d.currentRow() + delta) % d.count() next = unicode(d.item(next_index).data(Qt.DisplayRole).toString()) msg = '<p>' + _( 'No matches for %(text)s found in the current file [%(current)s].' ' Do you want to search in the %(which)s file [%(next)s]?') msg = msg % dict(text=text, current=current, next=next, which=_('next') if forwards else _('previous')) if question_dialog(self, _('No match found'), msg): self.pending_search = self.find_next if forwards else self.find_previous d.setCurrentRow(next_index) def find_next(self): return self.find() def find_previous(self): return self.find(forwards=False) def load(self, container): self.container = container spine_names = [ container.abspath_to_name(p) for p in container.spine_items ] spine_names = [n for n in spine_names if container.has_name(n)] self.dest_list.addItems(spine_names) def current_changed(self, item): name = self.current_name = unicode( item.data(Qt.DisplayRole).toString()) path = self.container.name_to_abspath(name) # Ensure encoding map is populated root = self.container.parsed(name) nasty = root.xpath('//*[local-name()="head"]/*[local-name()="p"]') if nasty: body = root.xpath('//*[local-name()="body"]') if not body: return error_dialog( self, _('Bad markup'), _('This book has severely broken markup, its ToC cannot be edited.' ), show=True) for x in reversed(nasty): body[0].insert(0, x) self.container.commit_item(name, keep_parsed=True) encoding = self.container.encoding_map.get(name, None) or 'utf-8' load_html(path, self.view, codec=encoding, mime_type=self.container.mime_map[name]) self.view.load_js() self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + name + '<br>' + _('Top of the file')) if hasattr(self, 'pending_search'): f = self.pending_search del self.pending_search f() def __call__(self, item, where): self.current_item, self.current_where = item, where self.current_name = None self.current_frag = None self.name.setText(_('(Untitled)')) dest_index, frag = 0, None if item is not None: if where is None: self.name.setText(item.data(0, Qt.DisplayRole).toString()) self.name.setCursorPosition(0) toc = item.data(0, Qt.UserRole).toPyObject() if toc.dest: for i in xrange(self.dest_list.count()): litem = self.dest_list.item(i) if unicode(litem.data( Qt.DisplayRole).toString()) == toc.dest: dest_index = i frag = toc.frag break self.dest_list.blockSignals(True) self.dest_list.setCurrentRow(dest_index) self.dest_list.blockSignals(False) item = self.dest_list.item(dest_index) self.current_changed(item) if frag: self.current_frag = frag QTimer.singleShot(1, self.show_frag) def show_frag(self): self.view.show_frag(self.current_frag) QTimer.singleShot(1, self.check_frag) def check_frag(self): pos = self.view.scroll_frac if pos == 0: self.current_frag = None self.update_dest_label() def get_loctext(self, frac): frac = int(round(frac * 100)) if frac == 0: loctext = _('Top of the file') else: loctext = _('Approximately %d%% from the top') % frac return loctext def elem_clicked(self, tag, frac, elem_id, loc): self.current_frag = elem_id or loc base = _('Location: A <%s> tag inside the file') % tag loctext = base + ' [%s]' % self.get_loctext(frac) self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + self.current_name + '<br>' + loctext) def update_dest_label(self): val = self.view.scroll_frac self.dest_label.setText(self.base_msg + '<br>' + _('File:') + ' ' + self.current_name + '<br>' + self.get_loctext(val)) @property def result(self): return (self.current_item, self.current_where, self.current_name, self.current_frag, unicode(self.name.text()))