def run_text_search(search, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file): try: pat = get_search_regex(search) except InvalidRegex as e: return error_dialog(gui_parent, _('Invalid regex'), '<p>' + _( 'The regular expression you entered is invalid: <pre>{0}</pre>With error: {1}').format( prepare_string_for_xml(e.regex), error_message(e)), show=True) editor, where, files, do_all, marked = initialize_search_request(search, 'count', current_editor, current_editor_name, searchable_names) with BusyCursor(): if editor is not None: if editor.find_text(pat): return True if not files and editor.find_text(pat, wrap=True): return True for fname, syntax in iteritems(files): ed = editors.get(fname, None) if ed is not None: if ed.find_text(pat, complete=True): show_editor(fname) return True else: root = current_container().parsed(fname) if hasattr(root, 'xpath'): raw = tostring(root, method='text', encoding='unicode', with_tail=True) else: raw = current_container().raw_data(fname) if pat.search(raw) is not None: edit_file(fname, syntax) if editors[fname].find_text(pat, complete=True): return True msg = '<p>' + _('No matches were found for %s') % ('<pre style="font-style:italic">' + prepare_string_for_xml(search['find']) + '</pre>') return error_dialog(gui_parent, _('Not found'), msg, show=True)
def add_files(self): if current_container() is None: return error_dialog(self.gui, _('No open book'), _( 'You must first open a book to tweak, before trying to create new files' ' in it.'), show=True) files = choose_files(self.gui, 'tweak-book-bulk-import-files', _('Choose files')) if files: folder_map = get_recommended_folders(current_container(), files) files = {x:('/'.join((folder, os.path.basename(x))) if folder else os.path.basename(x)) for x, folder in folder_map.iteritems()} self.commit_dirty_opf() self.add_savepoint(_('Add files')) c = current_container() for path, name in files.iteritems(): i = 0 while c.exists(name): i += 1 name, ext = name.rpartition('.')[0::2] name = '%s_%d.%s' % (name, i, ext) try: with open(path, 'rb') as f: c.add_file(name, f.read()) except: self.rewind_savepoint() raise self.gui.file_list.build(c) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) self.set_modified()
def add_file(self): if current_container() is None: return error_dialog(self.gui, _('No open book'), _( 'You must first open a book to tweak, before trying to create new files' ' in it.'), show=True) self.commit_dirty_opf() d = NewFileDialog(self.gui) if d.exec_() != d.Accepted: return self.add_savepoint(_('Add file %s') % self.gui.elided_text(d.file_name)) c = current_container() data = d.file_data if d.using_template: data = data.replace(b'%CURSOR%', b'') try: c.add_file(d.file_name, data) except: self.rewind_savepoint() raise self.gui.file_list.build(c) self.gui.file_list.select_name(d.file_name) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) mt = c.mime_map[d.file_name] syntax = syntax_from_mime(d.file_name, mt) if syntax: if d.using_template: self.edit_file(d.file_name, syntax, use_template=d.file_data.decode('utf-8')) else: self.edit_file(d.file_name, syntax)
def commit_editor_to_container(self, name, container=None): container = container or current_container() ed = editors[name] with container.open(name, 'wb') as f: f.write(ed.data) if container is current_container(): ed.is_synced_to_container = True
def editor_action(self, action): ed = self.gui.central.current_editor for n, x in editors.iteritems(): if x is ed: edname = n break if hasattr(ed, 'action_triggered'): if action and action[0] == 'insert_resource': rtype = action[1] if rtype == 'image' and ed.syntax not in {'css', 'html'}: return error_dialog(self.gui, _('Not supported'), _( 'Inserting images is only supported for HTML and CSS files.'), show=True) rdata = get_resource_data(rtype, self.gui) if rdata is None: return if rtype == 'image': chosen_name, chosen_image_is_external = rdata if chosen_image_is_external: with open(chosen_image_is_external[1], 'rb') as f: current_container().add_file(chosen_image_is_external[0], f.read()) self.refresh_file_list() chosen_name = chosen_image_is_external[0] href = current_container().name_to_href(chosen_name, edname) ed.insert_image(href) else: ed.action_triggered(action)
def do_all(replace=True): count = 0 if not files and editor is None: return 0 lfiles = files or {name:editor.syntax} for n, syntax in lfiles.iteritems(): if n in editors: raw = editors[n].get_raw_data() else: raw = current_container().raw_data(n) if replace: raw, num = pat.subn(state['replace'], raw) else: num = len(pat.findall(raw)) count += num if replace and num > 0: if n in editors: editors[n].replace_data(raw) else: with current_container().open(n, 'wb') as f: f.write(raw.encode('utf-8')) QApplication.restoreOverrideCursor() count_message(_('Replaced') if replace else _('Found'), count) return count
def check_requested(self, *args): if current_container() is None: return self.commit_all_editors_to_container() c = self.gui.check_book c.parent().show() c.parent().raise_() c.run_checks(current_container())
def reorder_spine(self, items): # TODO: If content.opf is dirty in an editor, abort, calling # file_list.build(current_container) to undo drag and drop self.add_savepoint(_('Re-order text')) c = current_container() c.set_spine(items) self.gui.action_save.setEnabled(True) self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items
def show_context_menu(self, point): item = self.itemAt(point) if item is None or item in set(self.categories.itervalues()): return m = QMenu(self) sel = self.selectedItems() num = len(sel) container = current_container() ci = self.currentItem() if ci is not None: cn = unicode(ci.data(0, NAME_ROLE) or '') mt = unicode(ci.data(0, MIME_ROLE) or '') cat = unicode(ci.data(0, CATEGORY_ROLE) or '') n = elided_text(cn.rpartition('/')[-1]) m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn)) if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS: m.addAction(_('Replace %s with file...') % n, partial(self.replace, cn)) if num > 1: m.addAction(QIcon(I('save.png')), _('Export all %d selected files') % num, self.export_selected) m.addSeparator() m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item) if is_raster_image(mt): m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn)) elif current_container().SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text': m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn)) m.addSeparator() if num > 0: m.addSeparator() if num > 1: m.addAction(QIcon(I('modified.png')), _('&Bulk rename the selected files'), self.request_bulk_rename) m.addAction(QIcon(I('modified.png')), _('Change the file extension for the selected files'), self.request_change_ext) m.addAction(QIcon(I('trash.png')), ngettext( '&Delete the selected file', '&Delete the {} selected files', num).format(num), self.request_delete) m.addAction(QIcon(I('edit-copy.png')), ngettext( '&Copy the selected file to another editor instance', '&Copy the {} selected files to another editor instance', num).format(num), self.copy_selected_files) m.addSeparator() selected_map = defaultdict(list) for item in sel: selected_map[unicode(item.data(0, CATEGORY_ROLE) or '')].append(unicode(item.data(0, NAME_ROLE) or '')) for items in selected_map.itervalues(): items.sort(key=self.index_of_name) if selected_map['text']: m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text'])) if len(selected_map['text']) > 1: m.addAction(QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text'])) if len(selected_map['styles']) > 1: m.addAction(QIcon(I('merge.png')), _('&Merge selected style files'), partial(self.start_merge, 'styles', selected_map['styles'])) if len(list(m.actions())) > 0: m.popup(self.mapToGlobal(point))
def reorder_spine(self, items): self.commit_dirty_opf() self.add_savepoint(_('Re-order text')) c = current_container() c.set_spine(items) self.set_modified() self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
def delete_selected(self): if self.DELETE_POSSIBLE: locations = self.selected_locations if locations: names = frozenset(locations) spine_names = {n for n, l in current_container().spine_names} other_items = names - spine_names spine_items = [(name, name in names) for name, is_linear in current_container().spine_names] self.delete_requested.emit(spine_items, other_items)
def edit_current_item(self): if not current_container().SUPPORTS_FILENAMES: error_dialog(self, _('Cannot rename'), _( '%s books do not support file renaming as they do not use file names' ' internally. The filenames you see are automatically generated from the' ' internal structures of the original file.') % current_container().book_type.upper(), show=True) return if self.currentItem() is not None: self.editItem(self.currentItem())
def reorder_spine(self, items): if not self.check_opf_dirtied(): return self.add_savepoint(_('Re-order text')) c = current_container() c.set_spine(items) self.gui.action_save.setEnabled(True) self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
def commit_editor_to_container(self, name, container=None): container = container or current_container() ed = editors[name] with container.open(name, 'wb') as f: f.write(ed.data) if name == container.opf_name: container.refresh_mime_map() if container is current_container(): ed.is_synced_to_container = True if name == container.opf_name: self.gui.file_list.build(container)
def mark_requested(self, name, action): self.commit_dirty_opf() c = current_container() if action == 'cover': mark_as_cover(current_container(), name) elif action.startswith('titlepage:'): action, move_to_start = action.partition(':')[0::2] move_to_start = move_to_start == 'True' mark_as_titlepage(current_container(), name, move_to_start=move_to_start) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) self.gui.file_list.build(c) self.set_modified()
def request_rename_common(self): if not current_container().SUPPORTS_FILENAMES: error_dialog(self, _('Cannot rename'), _( '%s books do not support file renaming as they do not use file names' ' internally. The filenames you see are automatically generated from the' ' internal structures of the original file.') % current_container().book_type.upper(), show=True) return names = {unicode(item.data(0, NAME_ROLE) or '') for item in self.selectedItems()} bad = names & current_container().names_that_must_not_be_changed if bad: error_dialog(self, _('Cannot rename'), _('The file(s) %s cannot be renamed.') % ('<b>%s</b>' % ', '.join(bad)), show=True) return names = sorted(names, key=self.index_of_name) return names
def replace(self, name): c = current_container() mt = c.mime_map[name] oext = name.rpartition(".")[-1].lower() filters = [oext] fname = _("Files") if mt in OEB_DOCS: fname = _("HTML Files") filters = "html htm xhtm xhtml shtml".split() elif is_raster_image(mt): fname = _("Images") filters = "jpeg jpg gif png".split() path = choose_files( self, "tweak_book_import_file", _("Choose file"), filters=[(fname, filters)], select_only_single_file=True ) if not path: return path = path[0] ext = path.rpartition(".")[-1].lower() force_mt = None if mt in OEB_DOCS: force_mt = c.guess_type("a.html") nname = os.path.basename(path) nname, ext = nname.rpartition(".")[0::2] nname = nname + "." + ext.lower() self.replace_requested.emit(name, path, nname, force_mt)
def get_completion_data(self, editor, ev=None): c = editor.textCursor() block, offset = c.block(), c.positionInBlock() oblock, boundary = next_tag_boundary(block, offset, forward=False, max_lines=5) if boundary is None or not boundary.is_start or boundary.closing: # Not inside a opening tag definition return tagname = boundary.name.lower() startpos = oblock.position() + boundary.offset c.setPosition(c.position()), c.setPosition(startpos, c.KeepAnchor) text = c.selectedText() m = self.complete_attr_pat.search(text) if m is None: return attr = m.group(1).lower().split(':')[-1] doc_name = editor.completion_doc_name if doc_name and attr in {'href', 'src'}: # A link query = m.group(2) or m.group(3) or '' c = current_container() names_type = {'a':'text_link', 'img':'image', 'image':'image', 'link':'stylesheet'}.get(tagname) idx = query.find('#') if idx > -1 and names_type in (None, 'text_link'): href, query = query[:idx], query[idx+1:] name = c.href_to_name(href) if href else doc_name if c.mime_map.get(name) in OEB_DOCS: return 'complete_anchor', name, query return 'complete_names', (names_type, doc_name, c.root), query
def fix_html(self): if self.syntax == "html": from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text(fix_html(current_container(), unicode(self.editor.toPlainText())).decode("utf-8")) return True return False
def set_data(name, val): if name in editors: editors[name].replace_data(val, only_if_different=False) else: with current_container().open(name, 'wb') as f: f.write(val) get_boss().set_modified()
def __init__(self, title=None, parent=None): QDialog.__init__(self, parent) t = title or current_container().mi.title self.book_title = t self.setWindowTitle(_('Edit the ToC in %s')%t) self.setWindowIcon(QIcon(I('toc.png'))) l = self.l = QVBoxLayout() self.setLayout(l) self.stacks = s = QStackedWidget(self) l.addWidget(s) self.toc_view = TOCView(self) self.toc_view.add_new_item.connect(self.add_new_item) s.addWidget(self.toc_view) self.item_edit = ItemEdit(self) s.addWidget(self.item_edit) bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) l.addWidget(bb) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.read_toc() self.resize(950, 630) geom = gprefs.get('toc_editor_window_geom', None) if geom is not None: self.restoreGeometry(bytes(geom))
def edit_file(self, name, syntax): editor = editors.get(name, None) if editor is None: editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs) data = current_container().raw_data(name) self.init_editor(name, editor, data) self.show_editor(name)
def update_editors_from_container(self, container=None): c = container or current_container() for name, ed in tuple(editors.iteritems()): if c.has_name(name): ed.replace_data(c.raw_data(name)) else: self.close_editor(name)
def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = {'css':pretty_css, 'xml':pretty_xml}.get(self.syntax, pretty_html) self.editor.replace_text(func(current_container(), name, unicode(self.editor.toPlainText())).decode('utf-8')) return True return False
def show(self, name): if name != self.current_name: self.refresh_timer.stop() self.current_name = name parse_worker.add_request(name) self.view.setUrl(QUrl.fromLocalFile(current_container().name_to_abspath(name))) return True
def add_file(self): if not self.check_opf_dirtied(): return d = NewFileDialog(self.gui) if d.exec_() != d.Accepted: return self.add_savepoint(_('Add file %s') % self.gui.elided_text(d.file_name)) c = current_container() data = d.file_data if d.using_template: data = data.replace(b'%CURSOR%', b'') try: c.add_file(d.file_name, data) except: self.rewind_savepoint() raise self.gui.file_list.build(c) self.gui.file_list.select_name(d.file_name) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) mt = c.mime_map[d.file_name] syntax = syntax_from_mime(mt) if syntax: if d.using_template: self.edit_file(d.file_name, syntax, use_template=d.file_data.decode('utf-8')) else: self.edit_file(d.file_name, syntax)
def link_stylesheets_requested(self, names, sheets): self.commit_all_editors_to_container() self.add_savepoint(_('Link stylesheets')) changed_names = link_stylesheets(current_container(), names, sheets) if changed_names: self.update_editors_from_container(names=changed_names) self.set_modified()
def check_opf_dirtied(self): c = current_container() if c.opf_name in editors and editors[c.opf_name].is_modified: return question_dialog(self.gui, _('Unsaved changes'), _( 'You have unsaved changes in %s. If you proceed,' ' you will lose them. Proceed anyway?') % c.opf_name) return True
def process_rule(self, rule, is_ancestor, 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))) ancestor_specificity = 0 if is_ancestor else 1 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, (0,0,0,0,0,0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith('file://'): href = href[len('file://'):] if iswindows and href.startswith('/'): href = href[1:] if href: rule['href'] = current_container().abspath_to_name(href, root=self.preview.current_root)
def setup_ui(self): self.l = l = QVBoxLayout(self) self.setLayout(l) self.msg = m = QLabel(self.msg or _( 'Choose the folder into which the files will be placed')) l.addWidget(m) m.setWordWrap(True) self.folders = f = QTreeWidget(self) f.setHeaderHidden(True) f.itemDoubleClicked.connect(self.accept) l.addWidget(f) f.setContextMenuPolicy(Qt.CustomContextMenu) f.customContextMenuRequested.connect(self.show_context_menu) self.root = QTreeWidgetItem(f, ('/',)) def process(node, parent): parent.setIcon(0, QIcon(I('mimetypes/dir.png'))) for child in sorted(node, key=numeric_sort_key): c = QTreeWidgetItem(parent, (child,)) process(node[child], c) process(create_folder_tree(current_container()), self.root) self.root.setSelected(True) f.expandAll() l.addWidget(self.bb)
def createRequest(self, operation, request, data): url = unicode(request.url().toString()) if operation == self.GetOperation and url.startswith('file://'): path = url[7:] if iswindows and path.startswith('/'): path = path[1:] c = current_container() name = c.abspath_to_name(path) if c.has_name(name): try: return NetworkReply( self, request, c.mime_map.get(name, 'application/octet-stream'), name) except Exception: import traceback traceback.print_exc() return QNetworkAccessManager.createRequest(self, operation, request, data)
def import_image(self): ans = choose_images(self, 'add-cover-choose-image', _('Choose a cover image'), formats=('jpg', 'jpeg', 'png', 'gif')) if ans: from calibre.gui2.tweak_book.file_list import NewFileDialog d = NewFileDialog(self) d.do_import_file(ans[0], hide_button=True) if d.exec_() == d.Accepted: self.import_requested.emit(d.file_name, d.file_data) self.container = current_container() self.names_filter.clear() self.names.model().set_names( sorted(self.image_names, key=sort_key)) i = self.names.model().find_name(d.file_name) self.names.setCurrentIndex(self.names.model().index(i)) self.current_image_changed()
def name_is_ok(self): name = unicode(self.name.text()) if not name or not name.strip(): return self.show_error('') ext = name.rpartition('.')[-1] if not ext or ext == name: return self.show_error(_('The file name must have an extension')) norm = name.replace('\\', '/') parts = name.split('/') for x in parts: if sanitize_file_name_unicode(x) != x: return self.show_error( _('The file name contains invalid characters')) if current_container().has_name(norm): return self.show_error( _('This file name already exists in the book')) self.show_error('') return True
def change_fonts(self): fonts = self.get_selected_data() if not fonts: return d = ChangeFontFamily( ', '.join(fonts), {f for f, embedded in iteritems(self.model.font_data) if embedded}, self) if d.exec_() != d.Accepted: return changed = False new_family = d.normalized_family for font in fonts: changed |= change_font(current_container(), font, new_family) if changed: self.model.build() self.container_changed.emit()
def build(self): c = current_container() if c is None: return toc = get_toc(c, verify_destinations=False) def process_node(toc, parent): for child in toc: node = QTreeWidgetItem(parent) node.setText(0, child.title or '') node.setData(0, DEST_ROLE, child.dest or '') node.setData(0, FRAG_ROLE, child.frag or '') tt = _('File: {0}\nAnchor: {1}').format( child.dest or '', child.frag or _('Top of file')) node.setData(0, Qt.ToolTipRole, tt) process_node(child, node) self.view.clear() process_node(toc, self.view.invisibleRootItem())
def sort_css(self): from calibre.gui2.dialogs.confirm_delete import confirm if confirm(_( 'Sorting CSS rules can in rare cases change the effective styles applied to the book.' ' Are you sure you want to proceed?'), 'edit-book-confirm-sort-css', parent=self, config_set=tprefs): c = self.textCursor() c.beginEditBlock() c.movePosition(c.Start), c.movePosition(c.End, c.KeepAnchor) text = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') from calibre.ebooks.oeb.polish.css import sort_sheet text = sort_sheet(current_container(), text).cssText c.insertText(text) c.movePosition(c.Start) c.endEditBlock() self.setTextCursor(c)
def get_droppable_files(self, md): def is_mt_ok(mt): return self.syntax == 'html' and ( mt in OEB_DOCS or mt in OEB_STYLES or mt.startswith('image/') ) if md.hasFormat(CONTAINER_DND_MIMETYPE): for line in as_unicode(bytes(md.data(CONTAINER_DND_MIMETYPE))).splitlines(): mt = current_container().mime_map.get(line, 'application/octet-stream') if is_mt_ok(mt): yield line, mt, True return for qurl in md.urls(): if qurl.isLocalFile() and os.access(qurl.toLocalFile(), os.R_OK): path = qurl.toLocalFile() mt = guess_type(path) if is_mt_ok(mt): yield path, mt, False
def request_delete(self): names = self.selected_names bad = names & current_container().names_that_must_not_be_removed if bad: return error_dialog(self, _('Cannot delete'), _('The file(s) %s cannot be deleted.') % ('<b>%s</b>' % ', '.join(bad)), show=True) text = self.categories['text'] children = (text.child(i) for i in range(text.childCount())) spine_removals = [(str(item.data(0, NAME_ROLE) or ''), item.isSelected()) for item in children] other_removals = { str(item.data(0, NAME_ROLE) or '') for item in self.selectedItems() if str(item.data(0, CATEGORY_ROLE) or '') != 'text' } self.delete_requested.emit(spine_removals, other_removals)
def do_find(): if editor is not None: if editor.find(pat, marked=marked): return if not files: if not state['wrap']: return no_match() return editor.find(pat, wrap=True, marked=marked) or no_match() for fname, syntax in files.iteritems(): if fname in editors: if not editors[fname].find(pat, complete=True): continue return self.show_editor(fname) raw = current_container().raw_data(fname) if pat.search(raw) is not None: self.edit_file(fname, syntax) if editors[fname].find(pat, complete=True): return return no_match()
def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, empty_index) # draw the hover and selection highlights name = unicode_type(index.data(Qt.DisplayRole) or '') cover = self.cover_cache.get(name, None) if cover is None: cover = self.cover_cache[name] = QPixmap() try: raw = current_container().raw_data(name, decode=False) except: pass else: try: dpr = painter.device().devicePixelRatioF() except AttributeError: dpr = painter.device().devicePixelRatio() cover.loadFromData(raw) cover.setDevicePixelRatio(dpr) if not cover.isNull(): scaled, width, height = fit_image(cover.width(), cover.height(), self.cover_size.width(), self.cover_size.height()) if scaled: cover = self.cover_cache[name] = cover.scaled(int(dpr*width), int(dpr*height), transformMode=Qt.SmoothTransformation) painter.save() try: rect = option.rect rect.adjust(self.MARGIN, self.MARGIN, -self.MARGIN, -self.MARGIN) trect = QRect(rect) rect.setBottom(rect.bottom() - self.title_height) if not cover.isNull(): dx = max(0, int((rect.width() - int(cover.width()/cover.devicePixelRatio()))/2.0)) dy = max(0, rect.height() - int(cover.height()/cover.devicePixelRatio())) rect.adjust(dx, dy, -dx, 0) painter.drawPixmap(rect, cover) rect = trect rect.setTop(rect.bottom() - self.title_height + 5) painter.setRenderHint(QPainter.TextAntialiasing, True) metrics = painter.fontMetrics() painter.drawText(rect, Qt.AlignCenter|Qt.TextSingleLine, metrics.elidedText(name, Qt.ElideLeft, rect.width())) finally: painter.restore()
def setup_ui(self): from calibre.ebooks.oeb.polish.images import get_compressible_images self.setWindowIcon(QIcon(I('compress-image.png'))) self.h = h = QHBoxLayout(self) self.images = i = QListWidget(self) h.addWidget(i) self.l = l = QVBoxLayout() h.addLayout(l) c = current_container() for name in sorted(get_compressible_images(c), key=numeric_sort_key): x = QListWidgetItem(name, i) x.setData(Qt.ItemDataRole.UserRole, c.filesize(name)) i.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) i.setMinimumHeight(350), i.setMinimumWidth(350) i.selectAll(), i.setSpacing(5) self.delegate = ImageItemDelegate(self) i.setItemDelegate(self.delegate) self.la = la = QLabel(_( 'You can compress the images in this book losslessly, reducing the file size of the book,' ' without affecting image quality. Typically image size is reduced by 5 - 15%.')) la.setWordWrap(True) la.setMinimumWidth(250) l.addWidget(la), l.addSpacing(30) self.enable_lossy = el = QCheckBox(_('Enable &lossy compression of JPEG images')) el.setToolTip(_('This allows you to change the quality factor used for JPEG images.\nBy lowering' ' the quality you can greatly reduce file size, at the expense of the image looking blurred.')) l.addWidget(el) self.h2 = h = QHBoxLayout() l.addLayout(h) self.jq = jq = QSpinBox(self) jq.setMinimum(0), jq.setMaximum(100), jq.setValue(tprefs.get('jpeg_compression_quality_for_lossless_compression', 80)), jq.setEnabled(False) jq.setToolTip(_('The compression quality, 1 is high compression, 100 is low compression.\nImage' ' quality is inversely correlated with compression quality.')) jq.valueChanged.connect(self.save_compression_quality) el.toggled.connect(jq.setEnabled) self.jql = la = QLabel(_('Compression &quality:')) la.setBuddy(jq) h.addWidget(la), h.addWidget(jq) l.addStretch(10) l.addWidget(self.bb)
def current_changed(self, current, previous): link = current.data(Qt.UserRole) if link is None: return url = None if link.is_external: if link.href: frag = ('#' + link.anchor.id) if link.anchor.id else '' url = QUrl(link.href + frag) elif link.anchor.location: path = current_container().name_to_abspath(link.anchor.location.name) if path and os.path.exists(path): url = QUrl.fromLocalFile(path) if link.anchor.id: url.setFragment(link.anchor.id) if url is None: self.view.setHtml('<p>' + _('No destination found for this link')) self.current_url = url elif url != self.current_url: self.current_url = url self.view.setUrl(url)
def createRequest(self, operation, request, data): url = unicode(request.url().toString(QUrl.None)) if operation == self.GetOperation and url.startswith('file://'): path = url[7:] if iswindows and path.startswith('/'): path = path[1:] c = current_container() try: name = c.abspath_to_name(path, root=self.current_root) except ValueError: # Happens on windows with absolute paths on different drives name = None if c.has_name(name): try: return NetworkReply( self, request, c.mime_map.get(name, 'application/octet-stream'), name) except Exception: import traceback traceback.print_exc() return QNetworkAccessManager.createRequest(self, operation, request, data)
def do_find(): for p, __ in searches: if editor is not None: if editor.find(p, marked=marked, save_match='gui'): return True if wrap and not files and editor.find(p, wrap=True, marked=marked, save_match='gui'): return True for fname, syntax in files.iteritems(): ed = editors.get(fname, None) if ed is not None: if not wrap and ed is editor: continue if ed.find(p, complete=True, save_match='gui'): show_editor(fname) return True else: raw = current_container().raw_data(fname) if p.search(raw) is not None: edit_file(fname, syntax) if editors[fname].find(p, complete=True, save_match='gui'): return True return no_match()
def request_bulk_rename(self): names = self.request_rename_common() if names is not None: categories = Counter(str(item.data(0, CATEGORY_ROLE) or '') for item in self.selectedItems()) settings = get_bulk_rename_settings(self, len(names), category=categories.most_common(1)[0][0], allow_spine_order=True) fmt, num = settings['prefix'], settings['start'] if fmt is not None: def change_name(name, num): parts = name.split('/') base, ext = parts[-1].rpartition('.')[0::2] parts[-1] = (fmt % num) + '.' + ext return '/'.join(parts) if settings['spine_order']: order_map = get_spine_order_for_all_files(current_container()) select_map = {n:i for i, n in enumerate(names)} def key(n): return order_map.get(n, (sys.maxsize, select_map[n])) name_map = {n: change_name(n, num + i) for i, n in enumerate(sorted(names, key=key))} else: name_map = {n:change_name(n, num + i) for i, n in enumerate(names)} self.bulk_rename_requested.emit(name_map)
def paint(self, painter, option, index): top_level = not index.parent().isValid() hover = option.state & QStyle.State_MouseOver if hover: if top_level: suffix = '%s(%d)' % (NBSP, index.model().rowCount(index)) else: try: suffix = NBSP + human_readable(current_container().filesize(unicode(index.data(NAME_ROLE) or ''))) except EnvironmentError: suffix = NBSP + human_readable(0) br = painter.boundingRect(option.rect, Qt.AlignRight|Qt.AlignVCenter, suffix) if top_level and index.row() > 0: option.rect.adjust(0, 5, 0, 0) painter.drawLine(option.rect.topLeft(), option.rect.topRight()) option.rect.adjust(0, 1, 0, 0) if hover: option.rect.adjust(0, 0, -br.width(), 0) QStyledItemDelegate.paint(self, painter, option, index) if hover: option.rect.adjust(0, 0, br.width(), 0) painter.drawText(option.rect, Qt.AlignRight|Qt.AlignVCenter, suffix)
def accept(self): if not self.name_is_ok: return error_dialog(self, _('No name specified'), _( 'You must specify a name for the new file, with an extension, for example, chapter1.html'), show=True) tprefs['auto_link_stylesheets'] = self.link_css.isChecked() name = str(self.name.text()) name, ext = name.rpartition('.')[0::2] name = (name + '.' + ext.lower()).replace('\\', '/') mt = guess_type(name) if not self.file_data: if mt in OEB_DOCS: self.file_data = template_for('html').encode('utf-8') if tprefs['auto_link_stylesheets']: data = add_stylesheet_links(current_container(), name, self.file_data) if data is not None: self.file_data = data self.using_template = True elif mt in OEB_STYLES: self.file_data = template_for('css').encode('utf-8') self.using_template = True self.file_name = name QDialog.accept(self)
def get_completion_data(self, editor, ev=None): c = editor.textCursor() block, offset = c.block(), c.positionInBlock() oblock, boundary = next_tag_boundary(block, offset, forward=False, max_lines=5) if boundary is None or not boundary.is_start or boundary.closing: # Not inside a opening tag definition return tagname = boundary.name.lower() startpos = oblock.position() + boundary.offset c.setPosition(c.position()), c.setPosition( startpos, QTextCursor.MoveMode.KeepAnchor) text = c.selectedText() m = self.complete_attr_pat.search(text) if m is None: return attr = m.group(1).lower().split(':')[-1] doc_name = editor.completion_doc_name if doc_name and attr in {'href', 'src'}: # A link query = m.group(2) or m.group(3) or '' c = current_container() names_type = { 'a': 'text_link', 'img': 'image', 'image': 'image', 'link': 'stylesheet' }.get(tagname) idx = query.find('#') if idx > -1 and names_type in (None, 'text_link'): href, query = query[:idx], query[idx + 1:] name = c.href_to_name(href) if href else doc_name if c.mime_map.get(name) in OEB_DOCS: return 'complete_anchor', name, query return 'complete_names', (names_type, doc_name, c.root), query
def __init__(self, title=None, parent=None): QDialog.__init__(self, parent) self.last_reject_at = self.last_accept_at = -1000 t = title or current_container().mi.title self.book_title = t self.setWindowTitle(_('Edit the ToC in %s') % t) self.setWindowIcon(QIcon(I('toc.png'))) l = self.l = QVBoxLayout() self.setLayout(l) self.stacks = s = QStackedWidget(self) l.addWidget(s) self.toc_view = TOCView(self, tprefs) self.toc_view.add_new_item.connect(self.add_new_item) s.addWidget(self.toc_view) self.item_edit = ItemEdit(self, tprefs) s.addWidget(self.item_edit) bb = self.bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) l.addWidget(bb) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.undo_button = b = bb.addButton( _('&Undo'), QDialogButtonBox.ButtonRole.ActionRole) b.setToolTip(_('Undo the last action, if any')) b.setIcon(QIcon(I('edit-undo.png'))) b.clicked.connect(self.toc_view.undo) self.read_toc() self.resize(950, 630) geom = tprefs.get('toc_editor_window_geom', None) if geom is not None: QApplication.instance().safe_restore_geometry(self, bytes(geom))
def paint(self, painter, option, index): name = index.data(Qt.ItemDataRole.DisplayRole) sz = human_readable(index.data(Qt.ItemDataRole.UserRole)) pmap = index.data(Qt.ItemDataRole.UserRole+1) irect = option.rect.adjusted(0, 5, 0, -5) irect.setRight(irect.left() + 70) if pmap is None: pmap = QPixmap(current_container().get_file_path_for_processing(name)) scaled, nwidth, nheight = fit_image(pmap.width(), pmap.height(), irect.width(), irect.height()) if scaled: pmap = pmap.scaled(nwidth, nheight, transformMode=Qt.TransformationMode.SmoothTransformation) index.model().setData(index, pmap, Qt.ItemDataRole.UserRole+1) x, y = (irect.width() - pmap.width())//2, (irect.height() - pmap.height())//2 r = irect.adjusted(x, y, -x, -y) QStyledItemDelegate.paint(self, painter, option, empty_index) painter.drawPixmap(r, pmap) trect = irect.adjusted(irect.width() + 10, 0, 0, 0) trect.setRight(option.rect.right()) painter.save() if option.state & QStyle.StateFlag.State_Selected: painter.setPen(QPen(option.palette.color(QPalette.ColorRole.HighlightedText))) painter.drawText(trect, Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, name + '\n' + sz) painter.restore()
def requestStarted(self, rq): if bytes(rq.requestMethod()) != b'GET': rq.fail(rq.RequestDenied) return url = rq.requestUrl() if url.host() != FAKE_HOST or url.scheme() != FAKE_PROTOCOL: rq.fail(rq.UrlNotFound) return name = url.path()[1:] try: if name.startswith('calibre_internal-mathjax/'): handle_mathjax_request(rq, name.partition('-')[-1]) return c = current_container() if not c.has_name(name): rq.fail(rq.UrlNotFound) return mime_type = c.mime_map.get(name, 'application/octet-stream') if mime_type in OEB_DOCS: mime_type = XHTML_MIME self.requests[name].append((mime_type, rq)) QTimer.singleShot(0, self.check_for_parse) else: data = get_data(name) if isinstance(data, unicode_type): data = data.encode('utf-8') mime_type = { # Prevent warning in console about mimetype of fonts 'application/vnd.ms-opentype': 'application/x-font-ttf', 'application/x-font-truetype': 'application/x-font-ttf', 'application/font-sfnt': 'application/x-font-ttf', }.get(mime_type, mime_type) send_reply(rq, mime_type, data) except Exception: import traceback traceback.print_exc() rq.fail(rq.RequestFailed)
def save_copy(self): c = current_container() ext = c.path_to_ebook.rpartition('.')[-1] path = choose_save_file(self.gui, 'tweak_book_save_copy', _('Choose path'), filters=[(_('Book (%s)') % ext.upper(), [ext.lower()])], all_files=False) if not path: return tdir = self.mkdtemp(prefix='save-copy-') container = clone_container(c, tdir) for name, ed in editors.iteritems(): if ed.is_modified or not ed.is_synced_to_container: self.commit_editor_to_container(name, container) def do_save(c, path, tdir): save_container(c, path) shutil.rmtree(tdir, ignore_errors=True) return path self.gui.blocking_job('save_copy', _('Saving copy, please wait...'), self.copy_saved, do_save, container, path, tdir)
def contextMenuEvent(self, ev): menu = QMenu(self) data = self._page.contextMenuData() url = data.linkUrl() url = unicode_type(url.toString(NO_URL_FORMATTING)).strip() text = data.selectedText() if text: ca = self.pageAction(QWebEnginePage.WebAction.Copy) if ca.isEnabled(): menu.addAction(ca) menu.addAction(actions['reload-preview']) menu.addAction(QIcon(I('debug.png')), _('Inspect element'), self.inspect) if url.partition(':')[0].lower() in {'http', 'https'}: menu.addAction(_('Open link'), partial(safe_open_url, data.linkUrl())) if QWebEngineContextMenuData.MediaType.MediaTypeImage <= data.mediaType( ) <= QWebEngineContextMenuData.MediaType.MediaTypeFile: url = data.mediaUrl() if url.scheme() == FAKE_PROTOCOL: href = url.path().lstrip('/') if href: c = current_container() resource_name = c.href_to_name(href) if resource_name and c.exists( resource_name ) and resource_name not in c.names_that_must_not_be_changed: self.add_open_with_actions(menu, resource_name) if data.mediaType( ) == QWebEngineContextMenuData.MediaType.MediaTypeImage: mime = c.mime_map[resource_name] if mime.startswith('image/'): menu.addAction( _('Edit %s') % resource_name, partial(self.edit_image, resource_name)) menu.exec_(ev.globalPos())
def replace(self, name): c = current_container() mt = c.mime_map[name] oext = name.rpartition('.')[-1].lower() filters = [oext] fname = _('Files') if mt in OEB_DOCS: fname = _('HTML Files') filters = 'html htm xhtm xhtml shtml'.split() elif is_raster_image(mt): fname = _('Images') filters = 'jpeg jpg gif png'.split() path = choose_files(self, 'tweak_book_import_file', _('Choose file'), filters=[(fname, filters)], select_only_single_file=True) if not path: return path = path[0] ext = path.rpartition('.')[-1].lower() force_mt = None if mt in OEB_DOCS: force_mt = c.guess_type('a.html') nname = os.path.basename(path) nname, ext = nname.rpartition('.')[0::2] nname = nname + '.' + ext.lower() self.replace_requested.emit(name, path, nname, force_mt)
def read_toc(self): self.toc_view(current_container()) self.item_edit.load(current_container()) self.stacks.setCurrentIndex(0)
def update_window_title(self): fname = os.path.basename(current_container().path_to_ebook) self.setWindowTitle( self.current_metadata.title + ' [%s] :: %s :: %s' % (current_container().book_type.upper(), fname, self.APP_NAME))
def write_toc(self): toc = self.toc_view.create_toc() commit_toc(current_container(), toc, lang=self.toc_view.toc_lang, uid=self.toc_view.toc_uid)
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) origc = QTextCursor(c) current_cursor = self.editor.textCursor() r = origr = self.editor.syntax_range_for_cursor(c) if ( r is None or not r.format.property(SPELL_PROPERTY) ) and c.positionInBlock() > 0 and not current_cursor.hasSelection(): c.setPosition(c.position() - 1) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY): word = self.editor.text_for_range(c.block(), r) locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) spell_cursor = self.editor.textCursor() if current_cursor.hasSelection(): # Restore the current cursor so that any selection is preserved # for the change case actions self.editor.setTextCursor(current_cursor) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction( suggestion, partial(self.editor.simple_replace, suggestion, cursor=spell_cursor)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction( _('Add this word to the dictionary: {0}').format( dics[0].name), partial(self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction( dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() if origr is not None and origr.format.property(LINK_PROPERTY): href = self.editor.text_for_range(origc.block(), origr) m.addAction( _('Open %s') % href, partial(self.link_clicked.emit, href)) if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)): word = self.editor.text_for_range(origc.block(), origr) item_type = 'tag_name' if origr.format.property( TAG_NAME_PROPERTY) else 'css_property' url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version) if url is not None: m.addAction( _('Show help for: %s') % word, partial(open_url, url)) for x in ('undo', 'redo'): ac = actions['editor-%s' % x] if ac.isEnabled(): a(ac) m.addSeparator() for x in ('cut', 'copy', 'paste'): ac = actions['editor-' + x] if ac.isEnabled(): a(ac) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) if self.selected_text or self.has_marked_text: update_mark_text_action(self) m.addAction(actions['mark-selected-text']) if self.syntax != 'css' and actions['editor-cut'].isEnabled(): cm = QMenu(_('Change &case'), m) for ac in 'upper lower swap title capitalize'.split(): cm.addAction(actions['transform-case-' + ac]) m.addMenu(cm) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.viewport().mapToGlobal(pos))
def show_context_menu(self, point): item = self.itemAt(point) if item is None or item in set(self.categories.itervalues()): return m = QMenu(self) sel = self.selectedItems() num = len(sel) container = current_container() ci = self.currentItem() if ci is not None: cn = unicode(ci.data(0, NAME_ROLE).toString()) mt = unicode(ci.data(0, MIME_ROLE).toString()) cat = unicode(ci.data(0, CATEGORY_ROLE).toString()) n = elided_text(cn.rpartition('/')[-1]) m.addAction(QIcon(I('save.png')), _('Export %s') % n, partial(self.export, cn)) if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS: m.addAction( _('Replace %s with file...') % n, partial(self.replace, cn)) m.addSeparator() m.addAction(QIcon(I('modified.png')), _('&Rename %s') % n, self.edit_current_item) if is_raster_image(mt): m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover image') % n, partial(self.mark_as_cover, cn)) elif current_container( ).SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text': m.addAction(QIcon(I('default_cover.png')), _('Mark %s as cover page') % n, partial(self.mark_as_titlepage, cn)) m.addSeparator() if num > 0: m.addSeparator() if num > 1: m.addAction(QIcon(I('modified.png')), _('&Bulk rename selected files'), self.request_bulk_rename) m.addAction(QIcon(I('trash.png')), _('&Delete the %d selected file(s)') % num, self.request_delete) m.addSeparator() selected_map = defaultdict(list) for item in sel: selected_map[unicode(item.data( 0, CATEGORY_ROLE).toString())].append( unicode(item.data(0, NAME_ROLE).toString())) for items in selected_map.itervalues(): items.sort(key=self.index_of_name) if selected_map['text']: m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text'])) if len(selected_map['text']) > 1: m.addAction( QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text'])) if len(selected_map['styles']) > 1: m.addAction( QIcon(I('merge.png')), _('&Merge selected style files'), partial(self.start_merge, 'styles', selected_map['styles'])) if len(list(m.actions())) > 0: m.popup(self.mapToGlobal(point))
def current_container(self): ' Return the current :class:`calibre.ebooks.oeb.polish.container.Container` object that represents the book being edited. ' return current_container()