class ProgressIndicator(QWidget): # {{{ def __init__(self, *args): QWidget.__init__(self, *args) self.setGeometry(0, 0, 300, 350) self.pi = _ProgressIndicator(self) self.status = QLabel(self) self.status.setWordWrap(True) self.status.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) self.setVisible(False) self.pos = None def start(self, msg=''): view = self.parent() pwidth, pheight = view.size().width(), view.size().height() self.resize(pwidth, min(pheight, 250)) if self.pos is None: self.move(0, int((pheight - self.size().height()) / 2)) else: self.move(self.pos[0], self.pos[1]) self.pi.resize(self.pi.sizeHint()) self.pi.move(int((self.size().width() - self.pi.size().width()) / 2), 0) self.status.resize(self.size().width(), self.size().height() - self.pi.size().height() - 10) self.status.move(0, self.pi.size().height() + 10) self.status.setText('<h1>' + msg + '</h1>') self.setVisible(True) self.pi.startAnimation() def stop(self): self.pi.stopAnimation() self.setVisible(False)
def setup_ui(self): self.l = l = QFormLayout(self) l.setFieldGrowthPolicy( QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) l.addRow( QLabel( _('The key of the identifier, for example, in isbn:XXX, the key is "isbn"' ))) self.key = k = QLineEdit(self) l.addRow(_('&Key:'), k) l.addRow( QLabel(_('The name that will appear in the Book details panel'))) self.nw = n = QLineEdit(self) l.addRow(_('&Name:'), n) la = QLabel( _('The template used to create the link.' ' The placeholder {0} in the template will be replaced' ' with the actual identifier value. Use {1} to avoid the value' ' being quoted.').format('{id}', '{id_unquoted}')) la.setWordWrap(True) l.addRow(la) self.template = t = QLineEdit(self) l.addRow(_('&Template:'), t) t.selectAll() t.setFocus(Qt.FocusReason.OtherFocusReason) l.addWidget(self.bb)
def setup_ui(self): self.l = l = QGridLayout() self.setLayout(l) self.la = la = QLabel(_( 'Arrange the files in this book into sub-folders based on their types.' ' If you leave a folder blank, the files will be placed in the root.')) la.setWordWrap(True) l.addWidget(la, 0, 0, 1, -1) folders = tprefs['folders_for_types'] for i, (typ, text) in enumerate(self.TYPE_MAP): la = QLabel('&' + text) setattr(self, '%s_label' % typ, la) le = QLineEdit(self) setattr(self, '%s_folder' % typ, le) val = folders.get(typ, '') if val and not val.endswith('/'): val += '/' le.setText(val) la.setBuddy(le) l.addWidget(la, i + 1, 0) l.addWidget(le, i + 1, 1) self.la2 = la = QLabel(_( 'Note that this will only arrange files inside the book,' ' it will not affect how they are displayed in the File browser')) la.setWordWrap(True) l.addWidget(la, i + 2, 0, 1, -1) l.addWidget(self.bb, i + 3, 0, 1, -1)
class MovedDialog(QDialog): # {{{ def __init__(self, stats, location, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('No library found')) self._l = l = QGridLayout(self) self.setLayout(l) self.stats, self.location = stats, location loc = self.oldloc = location.replace('/', os.sep) self.header = QLabel( _('No existing calibre library was found at %s. ' 'If the library was moved, select its new location below. ' 'Otherwise calibre will forget this library.') % loc) self.header.setWordWrap(True) ncols = 2 l.addWidget(self.header, 0, 0, 1, ncols) self.cl = QLabel('<b>' + _('New location of this library:')) l.addWidget(self.cl, l.rowCount(), 0, 1, ncols) self.loc = QLineEdit(loc, self) l.addWidget(self.loc, l.rowCount(), 0, 1, 1) self.cd = QToolButton(self) self.cd.setIcon(QIcon(I('document_open.png'))) self.cd.clicked.connect(self.choose_dir) l.addWidget(self.cd, l.rowCount() - 1, 1, 1, 1) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Abort) b = self.bb.addButton(_('Library moved'), QDialogButtonBox.ButtonRole.AcceptRole) b.setIcon(QIcon(I('ok.png'))) b = self.bb.addButton(_('Forget library'), QDialogButtonBox.ButtonRole.RejectRole) b.setIcon(QIcon(I('edit-clear.png'))) b.clicked.connect(self.forget_library) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) l.addWidget(self.bb, 3, 0, 1, ncols) self.resize(self.sizeHint() + QSize(120, 0)) def choose_dir(self): d = choose_dir(self, 'library moved choose new loc', _('New library location'), default_dir=self.oldloc) if d is not None: self.loc.setText(d) def forget_library(self): self.stats.remove(self.location) def accept(self): newloc = str(self.loc.text()) if not db_class().exists_at(newloc): error_dialog(self, _('No library found'), _('No existing calibre library found at %s') % newloc, show=True) return self.stats.rename(self.location, newloc) self.newloc = newloc QDialog.accept(self)
class LoadingOverlay(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout(self) self.pi = ProgressIndicator(self, 96) self.setVisible(False) self.label = QLabel(self) self.label.setText( '<i>testing with some long and wrap worthy message that should hopefully still render well' ) self.label.setTextFormat(Qt.TextFormat.RichText) self.label.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter) self.label.setWordWrap(True) if parent is None: self.resize(300, 300) else: self.resize(parent.size()) self.setAutoFillBackground(True) pal = self.palette() col = pal.color(QPalette.ColorRole.Window) col.setAlphaF(0.8) pal.setColor(QPalette.ColorRole.Window, col) self.setPalette(pal) self.move(0, 0) f = self.font() f.setBold(True) fm = QFontInfo(f) f.setPixelSize(int(fm.pixelSize() * 1.5)) self.label.setFont(f) l.addStretch(10) l.addWidget(self.pi) l.addWidget(self.label) l.addStretch(10) def __call__(self, msg=''): self.label.setText(msg) self.resize(self.parent().size()) self.move(0, 0) self.setVisible(True) self.raise_() self.setFocus(Qt.FocusReason.OtherFocusReason) self.update() def hide(self): self.parent().web_view.setFocus(Qt.FocusReason.OtherFocusReason) self.pi.stop() return QWidget.hide(self) def showEvent(self, ev): # import time # self.st = time.monotonic() self.pi.start() def hideEvent(self, ev): # import time # print(1111111, time.monotonic() - self.st) self.pi.stop()
def do_user_config(self, parent=None): ''' This method shows a configuration dialog for this plugin. It returns True if the user clicks OK, False otherwise. The changes are automatically applied. ''' from qt.core import (QDialog, QDialogButtonBox, QVBoxLayout, QLabel, Qt, QLineEdit, QCheckBox) config_dialog = QDialog(parent) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) v = QVBoxLayout(config_dialog) def size_dialog(): config_dialog.resize(config_dialog.sizeHint()) button_box.accepted.connect(config_dialog.accept) button_box.rejected.connect(config_dialog.reject) config_dialog.setWindowTitle(_('Customize') + ' ' + self.name) from calibre.customize.ui import (plugin_customization, customize_plugin) help_text = self.customization_help(gui=True) help_text = QLabel(help_text, config_dialog) help_text.setWordWrap(True) help_text.setTextInteractionFlags( Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard) help_text.setOpenExternalLinks(True) v.addWidget(help_text) bf = QCheckBox(_('Add linked files in breadth first order')) bf.setToolTip( _('Normally, when following links in HTML files' ' calibre does it depth first, i.e. if file A links to B and ' ' C, but B links to D, the files are added in the order A, B, D, C. ' ' With this option, they will instead be added as A, B, C, D')) sc = plugin_customization(self) if not sc: sc = '' sc = sc.strip() enc = sc.partition('|')[0] bfs = sc.partition('|')[-1] bf.setChecked(bfs == 'bf') sc = QLineEdit(enc, config_dialog) v.addWidget(sc) v.addWidget(bf) v.addWidget(button_box) size_dialog() config_dialog.exec() if config_dialog.result() == QDialog.DialogCode.Accepted: sc = str(sc.text()).strip() if bf.isChecked(): sc += '|bf' customize_plugin(self, sc) return config_dialog.result()
def add_row(x, y=None): if isinstance(x, unicode_type): x = QLabel(x) row = l.rowCount() if y is None: if isinstance(x, QLabel): x.setWordWrap(True) l.addWidget(x, row, 0, 1, 2) else: if isinstance(x, QLabel): x.setBuddy(y) l.addWidget(x, row, 0), l.addWidget(y, row, 1)
def __init__(self, ids, parent): QDialog.__init__(self, parent) self.setWindowTitle(_('Schedule download?')) self.setWindowIcon(QIcon(I('download-metadata.png'))) l = self.l = QGridLayout() self.setLayout(l) i = QLabel(self) i.setPixmap(QIcon(I('download-metadata.png')).pixmap(128, 128)) l.addWidget(i, 0, 0) t = ngettext( 'The download of metadata for the <b>selected book</b> will run in the background. Proceed?', 'The download of metadata for the <b>{} selected books</b> will run in the background. Proceed?', len(ids)).format(len(ids)) t = QLabel( '<p>' + t + '<p>' + _('You can monitor the progress of the download ' 'by clicking the rotating spinner in the bottom right ' 'corner.') + '<p>' + _('When the download completes you will be asked for' ' confirmation before calibre applies the downloaded metadata.')) t.setWordWrap(True) l.addWidget(t, 0, 1) l.setColumnStretch(0, 1) l.setColumnStretch(1, 100) self.identify = self.covers = True self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel) self.bb.rejected.connect(self.reject) b = self.bb.addButton(_('Download only &metadata'), QDialogButtonBox.ButtonRole.AcceptRole) b.clicked.connect(self.only_metadata) b.setIcon(QIcon(I('edit_input.png'))) b = self.bb.addButton(_('Download only &covers'), QDialogButtonBox.ButtonRole.AcceptRole) b.clicked.connect(self.only_covers) b.setIcon(QIcon(I('default_cover.png'))) b = self.b = self.bb.addButton(_('&Configure download'), QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon(I('config.png'))) connect_lambda(b.clicked, self, lambda self: show_config(self)) l.addWidget(self.bb, 1, 0, 1, 2) b = self.bb.addButton(_('Download &both'), QDialogButtonBox.ButtonRole.AcceptRole) b.clicked.connect(self.accept) b.setDefault(True) b.setAutoDefault(True) b.setIcon(QIcon(I('ok.png'))) self.resize(self.sizeHint()) b.setFocus(Qt.FocusReason.OtherFocusReason)
class DBCheck(QDialog): # {{{ update_msg = pyqtSignal(object) def __init__(self, parent, db): QDialog.__init__(self, parent) self.l = QVBoxLayout() self.setLayout(self.l) self.l1 = QLabel( _('Vacuuming database to improve performance.') + ' ' + _('This will take a while, please wait...')) self.setWindowTitle(_('Vacuuming...')) self.l1.setWordWrap(True) self.l.addWidget(self.l1) self.msg = QLabel('') self.update_msg.connect(self.msg.setText, type=Qt.ConnectionType.QueuedConnection) self.l.addWidget(self.msg) self.msg.setWordWrap(True) self.resize(self.sizeHint() + QSize(100, 50)) self.error = None self.db = db.new_api self.rejected = False def start(self): t = self.thread = Thread(target=self.vacuum) t.daemon = True t.start() QTimer.singleShot(100, self.check) self.exec_() def vacuum(self): try: self.db.vacuum() except Exception as e: import traceback self.error = (as_unicode(e), traceback.format_exc()) def reject(self): self.rejected = True return QDialog.reject(self) def check(self): if self.rejected: return if self.thread.is_alive(): QTimer.singleShot(100, self.check) else: self.accept() def break_cycles(self): self.db = self.thread = None
def view_server_logs(self): from calibre.srv.embedded import log_paths log_error_file, log_access_file = log_paths() d = QDialog(self) d.resize(QSize(800, 600)) layout = QVBoxLayout() d.setLayout(layout) layout.addWidget(QLabel(_('Error log:'))) el = QPlainTextEdit(d) layout.addWidget(el) try: el.setPlainText( share_open(log_error_file, 'rb').read().decode('utf8', 'replace') ) except EnvironmentError: el.setPlainText(_('No error log found')) layout.addWidget(QLabel(_('Access log:'))) al = QPlainTextEdit(d) layout.addWidget(al) try: al.setPlainText( share_open(log_access_file, 'rb').read().decode('utf8', 'replace') ) except EnvironmentError: al.setPlainText(_('No access log found')) loc = QLabel(_('The server log files are in: {}').format(os.path.dirname(log_error_file))) loc.setWordWrap(True) layout.addWidget(loc) bx = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) layout.addWidget(bx) bx.accepted.connect(d.accept) b = bx.addButton(_('&Clear logs'), QDialogButtonBox.ButtonRole.ActionRole) def clear_logs(): if getattr(self.server, 'is_running', False): return error_dialog(d, _('Server running'), _( 'Cannot clear logs while the server is running. First stop the server.'), show=True) if self.server: self.server.access_log.clear() self.server.log.clear() else: for x in (log_error_file, log_access_file): try: os.remove(x) except EnvironmentError as err: if err.errno != errno.ENOENT: raise el.setPlainText(''), al.setPlainText('') b.clicked.connect(clear_logs) d.show()
class SelectFormats(QDialog): def __init__(self, fmt_count, msg, single=False, parent=None, exclude=False): QDialog.__init__(self, parent) self._l = QVBoxLayout(self) self.single_fmt = single self.setLayout(self._l) self.setWindowTitle(_('Choose formats')) self._m = QLabel(msg) self._m.setWordWrap(True) self._l.addWidget(self._m) self.formats = Formats(fmt_count) self.fview = QListView(self) self.fview.doubleClicked.connect( self.double_clicked, type=Qt.ConnectionType.QueuedConnection) if exclude: if QApplication.instance().is_dark_theme: sheet = 'background-color: #DAA520; color: black' else: sheet = 'background-color: #fae7b5' self.fview.setStyleSheet('QListView { %s }' % sheet) self._l.addWidget(self.fview) self.fview.setModel(self.formats) self.fview.setSelectionMode( QAbstractItemView.SelectionMode.SingleSelection if single else QAbstractItemView.SelectionMode.MultiSelection) self.bbox = \ QDialogButtonBox(QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel, Qt.Orientation.Horizontal, self) self._l.addWidget(self.bbox) self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) self.fview.setIconSize(QSize(48, 48)) self.fview.setSpacing(2) self.resize(350, 500) self.selected_formats = set() def accept(self, *args): for idx in self.fview.selectedIndexes(): self.selected_formats.add(self.formats.fmt(idx)) QDialog.accept(self, *args) def double_clicked(self, index): if self.single_fmt: self.accept()
def __init__(self, parent=None): QWidget.__init__(self, parent) self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.l = l = QVBoxLayout(self) l.setContentsMargins(0, 0, 0, 0) self.search_input = si = SearchInput(self, 'highlights-search') si.do_search.connect(self.search_requested) l.addWidget(si) la = QLabel(_('Double click to jump to an entry')) la.setWordWrap(True) l.addWidget(la) self.highlights = h = Highlights(self) l.addWidget(h) h.jump_to_highlight.connect(self.jump_to_highlight) h.delete_requested.connect(self.remove_highlight) h.edit_requested.connect(self.edit_highlight) h.edit_notes_requested.connect(self.edit_notes) h.current_highlight_changed.connect(self.current_highlight_changed) self.load = h.load self.refresh = h.refresh self.h = h = QHBoxLayout() def button(icon, text, tt, target): b = QPushButton(QIcon(I(icon)), text, self) b.setToolTip(tt) b.setFocusPolicy(Qt.FocusPolicy.NoFocus) b.clicked.connect(target) return b self.edit_button = button('edit_input.png', _('Modify'), _('Modify the selected highlight'), self.edit_highlight) self.remove_button = button('trash.png', _('Delete'), _('Delete the selected highlights'), self.remove_highlight) self.export_button = button('save.png', _('Export'), _('Export all highlights'), self.export) h.addWidget(self.edit_button), h.addWidget( self.remove_button), h.addWidget(self.export_button) self.notes_display = nd = NotesDisplay(self) nd.notes_edited.connect(self.notes_edited) l.addWidget(nd) nd.setVisible(False) l.addLayout(h)
def setup_ui(self): from calibre.gui2.tweak_book.templates import DEFAULT_TEMPLATES from calibre.gui2.tweak_book.editor.text import TextEdit # Cannot use QFormLayout as it does not play nice with TextEdit on windows self.l = l = QVBoxLayout(self) self.syntaxes = s = QComboBox(self) s.addItems(sorted(DEFAULT_TEMPLATES)) s.setCurrentIndex(s.findText('html')) h = QHBoxLayout() l.addLayout(h) la = QLabel(_('Choose the &type of template to edit:')) la.setBuddy(s) h.addWidget(la), h.addWidget(s), h.addStretch(10) s.currentIndexChanged.connect(self.show_template) self.helpl = la = QLabel( _('The variables {0} and {1} will be replaced with the title and author of the book. {2}' ' is where the cursor will be positioned. If you want to include braces in your template,' ' for example for CSS rules, you have to escape them, like this: {3}' ).format(*[ '<code>%s</code>' % x for x in ['{TITLE}', '{AUTHOR}', '%CURSOR%', 'body {{ color: red }}'] ])) la.setWordWrap(True) l.addWidget(la) self.save_timer = t = QTimer(self) t.setSingleShot(True), t.setInterval(100) t.timeout.connect(self._save_syntax) self.editor = e = TextEdit(self) l.addWidget(e) e.textChanged.connect(self.save_syntax) self.show_template() self.bb.clear() self.bb.addButton(QDialogButtonBox.StandardButton.Close) self.rd = b = self.bb.addButton( QDialogButtonBox.StandardButton.RestoreDefaults) b.clicked.connect(self.restore_defaults) l.addWidget(self.bb)
def create_template_widget(title, which, button): attr = which + '_template' heading = QLabel('<h2>' + title) setattr(tp, attr + '_heading', heading) l.addWidget(heading) la = QLabel() setattr(self, attr, la) l.addWidget(la), la.setTextFormat(Qt.TextFormat.PlainText), la.setStyleSheet('QLabel {font-family: monospace}') la.setWordWrap(True) b = QPushButton(button) b.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) connect_lambda(b.clicked, self, lambda self: self.change_template(which)) setattr(self, attr + '_button', b) l.addWidget(b) if which != 'footer': f = QFrame(tp) setattr(tp, attr + '_sep', f), f.setFrameShape(QFrame.Shape.HLine) l.addWidget(f) l.addSpacing(10)
class BackupStatus(QDialog): # {{{ def __init__(self, gui): QDialog.__init__(self, gui) self.l = l = QVBoxLayout(self) self.msg = QLabel('') self.msg.setWordWrap(True) l.addWidget(self.msg) self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) b = bb.addButton(_('Queue &all books for backup'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.mark_all_dirty) b.setIcon(QIcon(I('lt.png'))) l.addWidget(bb) self.db = weakref.ref(gui.current_db) self.setResult(9) self.setWindowTitle(_('Backup status')) self.update() self.resize(self.sizeHint() + QSize(50, 15)) def update(self): db = self.db() if db is None: return if self.result() != 9: return dirty_text = 'no' try: dirty_text = '%s' % db.dirty_queue_length() except: dirty_text = _('none') self.msg.setText('<p>' + _('Book metadata files remaining to be written: %s') % dirty_text) QTimer.singleShot(1000, self.update) def mark_all_dirty(self): db = self.db() if db is None: return db.new_api.mark_as_dirty(db.new_api.all_book_ids())
class EveryXDays(Base): HELP = _('''\ Download this periodical every x days. For example, if you choose 30 days, the periodical will be downloaded every 30 days. Note that you can set periods of less than a day, like 0.1 days to download a periodical more than once a day. ''') def __init__(self, parent=None): Base.__init__(self, parent) self.l1 = QLabel(_('&Download every:')) self.interval = QDoubleSpinBox(self) self.interval.setMinimum(0.04) self.interval.setSpecialValueText(_('every hour')) self.interval.setMaximum(1000.0) self.interval.setValue(31.0) self.interval.setSuffix(' ' + _('days')) self.interval.setSingleStep(1.0) self.interval.setDecimals(2) self.l1.setBuddy(self.interval) self.l2 = QLabel( _('Note: You can set intervals of less than a day,' ' by typing the value manually.')) self.l2.setWordWrap(True) self.l.addWidget(self.l1, 0, 0, 1, 1) self.l.addWidget(self.interval, 0, 1, 1, 1) self.l.addWidget(self.l2, 1, 0, 1, -1) def initialize(self, typ=None, val=None): if val is None: val = 31.0 self.interval.setValue(val) @property def schedule(self): schedule = self.interval.value() return 'interval', schedule
def setup_ui(self): self.l = l = QFormLayout(self) self.name_edit = n = QLineEdit(self) n.setPlaceholderText(_('The name of the source')) n.setMinimumWidth(450) l.addRow(_('&Name:'), n) if self.initial_name: n.setText(self.initial_name) n.setReadOnly(True) self.url_edit = u = QLineEdit(self) u.setPlaceholderText(_('The URL template of the source')) u.setMinimumWidth(n.minimumWidth()) l.addRow(_('&URL:'), u) if self.initial_url: u.setText(self.initial_url) la = QLabel( _('The URL template must starts with https:// and have {word} in it which will be replaced by the actual query' )) la.setWordWrap(True) l.addRow(la) l.addRow(self.bb) if self.initial_name: u.setFocus(Qt.FocusReason.OtherFocusReason)
class PluginTweaks(QDialog): # {{{ def __init__(self, raw, parent=None): QDialog.__init__(self, parent) self.setWindowTitle(_('Plugin tweaks')) self.edit = QPlainTextEdit(self) self.highlighter = PythonHighlighter(self.edit.document()) self.l = QVBoxLayout() self.setLayout(self.l) self.msg = QLabel( _('Add/edit tweaks for any custom plugins you have installed. ' 'Documentation for these tweaks should be available ' 'on the website from where you downloaded the plugins.')) self.msg.setWordWrap(True) self.l.addWidget(self.msg) self.l.addWidget(self.edit) self.edit.setPlainText(raw) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel, Qt.Orientation.Horizontal, self) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) self.l.addWidget(self.bb) self.resize(550, 300)
class ConfigWidget(QWidget): def __init__(self, plugin): QWidget.__init__(self) self.plugin = plugin self.overl = l = QVBoxLayout(self) self.gb = QGroupBox(_('Metadata fields to download'), self) if plugin.config_help_message: self.pchm = QLabel(plugin.config_help_message) self.pchm.setWordWrap(True) self.pchm.setOpenExternalLinks(True) l.addWidget(self.pchm, 10) l.addWidget(self.gb) self.gb.l = g = QVBoxLayout(self.gb) g.setContentsMargins(0, 0, 0, 0) self.fields_view = v = FieldsList(self) g.addWidget(v) v.setFlow(QListView.Flow.LeftToRight) v.setWrapping(True) v.setResizeMode(QListView.ResizeMode.Adjust) self.fields_model = FieldsModel(self.plugin) self.fields_model.initialize() v.setModel(self.fields_model) self.memory = [] self.widgets = [] self.l = QGridLayout() self.l.setContentsMargins(0, 0, 0, 0) l.addLayout(self.l, 100) for opt in plugin.options: self.create_widgets(opt) def create_widgets(self, opt): val = self.plugin.prefs[opt.name] if opt.type == 'number': c = QSpinBox if isinstance(opt.default, numbers.Integral) else QDoubleSpinBox widget = c(self) widget.setValue(val) elif opt.type == 'string': widget = QLineEdit(self) widget.setText(val if val else '') elif opt.type == 'bool': widget = QCheckBox(opt.label, self) widget.setChecked(bool(val)) elif opt.type == 'choices': widget = QComboBox(self) items = list(iteritems(opt.choices)) items.sort(key=lambda k_v: sort_key(k_v[1])) for key, label in items: widget.addItem(label, (key)) idx = widget.findData(val) widget.setCurrentIndex(idx) widget.opt = opt widget.setToolTip(textwrap.fill(opt.desc)) self.widgets.append(widget) r = self.l.rowCount() if opt.type == 'bool': self.l.addWidget(widget, r, 0, 1, self.l.columnCount()) else: l = QLabel(opt.label) l.setToolTip(widget.toolTip()) self.memory.append(l) l.setBuddy(widget) self.l.addWidget(l, r, 0, 1, 1) self.l.addWidget(widget, r, 1, 1, 1) def commit(self): self.fields_model.commit() for w in self.widgets: if isinstance(w, (QSpinBox, QDoubleSpinBox)): val = w.value() elif isinstance(w, QLineEdit): val = str(w.text()) elif isinstance(w, QCheckBox): val = w.isChecked() elif isinstance(w, QComboBox): idx = w.currentIndex() val = str(w.itemData(idx) or '') self.plugin.prefs[w.opt.name] = val
class JobError(QDialog): # {{{ WIDTH = 600 do_pop = pyqtSignal() def __init__(self, parent): QDialog.__init__(self, parent) self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False) self.queue = [] self.do_pop.connect(self.pop, type=Qt.ConnectionType.QueuedConnection) self._layout = l = QGridLayout() self.setLayout(l) self.icon = QIcon(I('dialog_error.png')) self.setWindowIcon(self.icon) self.icon_widget = Icon(self) self.icon_widget.set_icon(self.icon) self.msg_label = QLabel('<p> ') self.msg_label.setStyleSheet('QLabel { margin-top: 1ex; }') self.msg_label.setWordWrap(True) self.msg_label.setTextFormat(Qt.TextFormat.RichText) self.det_msg = QPlainTextEdit(self) self.det_msg.setVisible(False) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, parent=self) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) self.ctc_button = self.bb.addButton( _('&Copy to clipboard'), QDialogButtonBox.ButtonRole.ActionRole) self.ctc_button.clicked.connect(self.copy_to_clipboard) self.retry_button = self.bb.addButton( _('&Retry'), QDialogButtonBox.ButtonRole.ActionRole) self.retry_button.clicked.connect(self.retry) self.retry_func = None self.show_det_msg = _('Show &details') self.hide_det_msg = _('Hide &details') self.det_msg_toggle = self.bb.addButton( self.show_det_msg, QDialogButtonBox.ButtonRole.ActionRole) self.det_msg_toggle.clicked.connect(self.toggle_det_msg) self.det_msg_toggle.setToolTip( _('Show detailed information about this error')) self.suppress = QCheckBox(self) l.addWidget(self.icon_widget, 0, 0, 1, 1) l.addWidget(self.msg_label, 0, 1, 1, 1) l.addWidget(self.det_msg, 1, 0, 1, 2) l.addWidget(self.suppress, 2, 0, 1, 2, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom) l.addWidget(self.bb, 3, 0, 1, 2, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignBottom) l.setColumnStretch(1, 100) self.setModal(False) self.suppress.setVisible(False) self.do_resize() def retry(self): if self.retry_func is not None: self.accept() self.retry_func() def update_suppress_state(self): self.suppress.setText( ngettext('Hide the remaining error message', 'Hide the {} remaining error messages', len(self.queue)).format(len(self.queue))) self.suppress.setVisible(len(self.queue) > 3) self.do_resize() def copy_to_clipboard(self, *args): d = QTextDocument() d.setHtml(self.msg_label.text()) QApplication.clipboard().setText( 'calibre, version %s (%s, embedded-python: %s)\n%s: %s\n\n%s' % (__version__, sys.platform, isfrozen, str(self.windowTitle()), str(d.toPlainText()), str(self.det_msg.toPlainText()))) if hasattr(self, 'ctc_button'): self.ctc_button.setText(_('Copied')) def toggle_det_msg(self, *args): vis = str(self.det_msg_toggle.text()) == self.hide_det_msg self.det_msg_toggle.setText( self.show_det_msg if vis else self.hide_det_msg) self.det_msg.setVisible(not vis) self.do_resize() def do_resize(self): h = self.sizeHint().height() self.setMinimumHeight(0) # Needed as this gets set if det_msg is shown # Needed otherwise re-showing the box after showing det_msg causes the box # to not reduce in height self.setMaximumHeight(h) self.resize(QSize(self.WIDTH, h)) def showEvent(self, ev): ret = QDialog.showEvent(self, ev) self.bb.button(QDialogButtonBox.StandardButton.Close).setFocus( Qt.FocusReason.OtherFocusReason) return ret def show_error(self, title, msg, det_msg='', retry_func=None): self.queue.append((title, msg, det_msg, retry_func)) self.update_suppress_state() self.pop() def pop(self): if not self.queue or self.isVisible(): return title, msg, det_msg, retry_func = self.queue.pop(0) self.setWindowTitle(title) self.msg_label.setText(msg) self.det_msg.setPlainText(det_msg) self.det_msg.setVisible(False) self.det_msg_toggle.setText(self.show_det_msg) self.det_msg_toggle.setVisible(True) self.suppress.setChecked(False) self.update_suppress_state() if not det_msg: self.det_msg_toggle.setVisible(False) self.retry_button.setVisible(retry_func is not None) self.retry_func = retry_func self.do_resize() self.show() def done(self, r): if self.suppress.isChecked(): self.queue = [] QDialog.done(self, r) self.do_pop.emit()
def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.Shape.NoFrame if gprefs['tag_browser_old_look'] else QFrame.Shape.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0,0,0,0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) tbb.clear_find.connect(self.reset_find) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.Shape.StyledPanel) l.setAutoFillBackground(True) l.setText('<p><b>'+_('No more matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignmentFlag.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, type=Qt.ConnectionType.QueuedConnection) self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) # The Configure Tag Browser button l = self.alter_tb ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) l.m.aboutToShow.connect(self.about_to_show_configure_menu) l.m.show_counts_action = ac = l.m.addAction('counts') ac.triggered.connect(self.toggle_counts) l.m.show_avg_rating_action = ac = l.m.addAction('avg rating') ac.triggered.connect(self.toggle_avg_rating) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip( _('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip(_('All of these category_managers are available by right-clicking ' 'on items in the Tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser set focus', _("Give the Tag browser keyboard focus"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.give_tb_focus)
def label(text): la = QLabel(text) la.setWordWrap(True), l.addWidget(la), la.setMinimumWidth(450) l.addWidget(la) return la
def __init__(self, extra_customization_message, extra_customization_choices, device_settings): super(ExtraCustomization, self).__init__() debug_print( "ExtraCustomization.__init__ - extra_customization_message=", extra_customization_message) debug_print( "ExtraCustomization.__init__ - extra_customization_choices=", extra_customization_choices) debug_print( "ExtraCustomization.__init__ - device_settings.extra_customization=", device_settings.extra_customization) debug_print("ExtraCustomization.__init__ - device_settings=", device_settings) self.extra_customization_message = extra_customization_message self.l = QVBoxLayout(self) self.setLayout(self.l) options_group = QGroupBox(_("Extra driver customization options"), self) self.l.addWidget(options_group) self.extra_layout = QGridLayout() self.extra_layout.setObjectName("extra_layout") options_group.setLayout(self.extra_layout) 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(device_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(device_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( device_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( device_settings.extra_customization[i]) self.opt_extra_customization[i].setCursorPosition(0) self.extra_layout.addWidget(l, row_func(i + 2, 0), col_func(i)) self.extra_layout.addWidget( self.opt_extra_customization[i], row_func(i + 2, 1), col_func(i)) spacerItem1 = QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.extra_layout.addItem(spacerItem1, row_func(i + 2 + 2, 1), 0, 1, 2) self.extra_layout.setRowStretch(row_func(i + 2 + 2, 1), 2) 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 device_settings.extra_customization: self.opt_extra_customization.setText( device_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)
class UpdateNotification(QDialog): def __init__(self, calibre_version, plugin_updates, parent=None): QDialog.__init__(self, parent) self.setAttribute(Qt.WidgetAttribute.WA_QuitOnClose, False) self.resize(400, 250) self.l = QGridLayout() self.setLayout(self.l) self.logo = QLabel() self.logo.setMaximumWidth(110) self.logo.setPixmap(QIcon(I('lt.png')).pixmap(100, 100)) ver = calibre_version if ver.endswith('.0'): ver = ver[:-2] self.label = QLabel( '<p>' + _('New version <b>{ver}</b> of {app} is available for download. ' 'See the <a href="{url}">new features</a>.').format( url=localize_website_link( 'https://calibre-ebook.com/whats-new'), app=__appname__, ver=ver)) self.label.setOpenExternalLinks(True) self.label.setWordWrap(True) self.setWindowTitle(_('Update available!')) self.setWindowIcon(QIcon(I('lt.png'))) self.l.addWidget(self.logo, 0, 0) self.l.addWidget(self.label, 0, 1) self.cb = QCheckBox(_('Show this notification for future updates'), self) self.l.addWidget(self.cb, 1, 0, 1, -1) self.cb.setChecked(config.get('new_version_notification')) self.cb.stateChanged.connect(self.show_future) self.bb = QDialogButtonBox(self) b = self.bb.addButton(_('&Get update'), QDialogButtonBox.ButtonRole.AcceptRole) b.setDefault(True) b.setIcon(QIcon(I('arrow-down.png'))) if plugin_updates > 0: b = self.bb.addButton(_('Update &plugins'), QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon(I('plugins/plugin_updater.png'))) b.clicked.connect(self.get_plugins, type=Qt.ConnectionType.QueuedConnection) self.bb.addButton(QDialogButtonBox.StandardButton.Cancel) self.l.addWidget(self.bb, 2, 0, 1, -1) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) save_version_notified(calibre_version) def get_plugins(self): from calibre.gui2.dialogs.plugin_updater import ( PluginUpdaterDialog, FILTER_UPDATE_AVAILABLE) d = PluginUpdaterDialog(self.parent(), initial_filter=FILTER_UPDATE_AVAILABLE) d.exec() if d.do_restart: QDialog.accept(self) from calibre.gui2.ui import get_gui gui = get_gui() if gui is not None: gui.quit(restart=True) def show_future(self, *args): config.set('new_version_notification', bool(self.cb.isChecked())) def accept(self): open_url(QUrl(get_download_url())) QDialog.accept(self)
def do_user_config(self, parent=None): ''' This method shows a configuration dialog for this plugin. It returns True if the user clicks OK, False otherwise. The changes are automatically applied. ''' from qt.core import QDialog, QDialogButtonBox, QVBoxLayout, \ QLabel, Qt, QLineEdit from calibre.gui2 import gprefs prefname = 'plugin config dialog:' + self.type + ':' + self.name geom = gprefs.get(prefname, None) config_dialog = QDialog(parent) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) v = QVBoxLayout(config_dialog) def size_dialog(): if geom is None: config_dialog.resize(config_dialog.sizeHint()) else: from qt.core import QApplication QApplication.instance().safe_restore_geometry( config_dialog, geom) button_box.accepted.connect(config_dialog.accept) button_box.rejected.connect(config_dialog.reject) config_dialog.setWindowTitle(_('Customize') + ' ' + self.name) try: config_widget = self.config_widget() except NotImplementedError: config_widget = None if isinstance(config_widget, tuple): from calibre.gui2 import warning_dialog warning_dialog(parent, _('Cannot configure'), config_widget[0], det_msg=config_widget[1], show=True) return False if config_widget is not None: v.addWidget(config_widget) v.addWidget(button_box) size_dialog() config_dialog.exec() if config_dialog.result() == QDialog.DialogCode.Accepted: if hasattr(config_widget, 'validate'): if config_widget.validate(): self.save_settings(config_widget) else: self.save_settings(config_widget) else: from calibre.customize.ui import plugin_customization, \ customize_plugin help_text = self.customization_help(gui=True) help_text = QLabel(help_text, config_dialog) help_text.setWordWrap(True) help_text.setTextInteractionFlags( Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard) help_text.setOpenExternalLinks(True) v.addWidget(help_text) sc = plugin_customization(self) if not sc: sc = '' sc = sc.strip() sc = QLineEdit(sc, config_dialog) v.addWidget(sc) v.addWidget(button_box) size_dialog() config_dialog.exec() if config_dialog.result() == QDialog.DialogCode.Accepted: sc = str(sc.text()).strip() customize_plugin(self, sc) geom = bytearray(config_dialog.saveGeometry()) gprefs[prefname] = geom return config_dialog.result()
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 = all_formats | set(BOOK_EXTENSIONS) format_map = settings.format_map disabled_formats = all_formats.difference(format_map) for format in format_map + sorted(disabled_formats): item = QListWidgetItem(format, self.columns) item.setData(Qt.ItemDataRole.UserRole, (format)) item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsSelectable) item.setCheckState(Qt.CheckState.Checked if format in format_map else Qt.CheckState.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)
class RelaySetup(QDialog): def __init__(self, service, parent): QDialog.__init__(self, parent) self.l = l = QGridLayout() self.setLayout(l) self.bb = bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.tl = QLabel(('<p>' + _( 'Setup sending email using' ) + ' <b>{name}</b><p>' + _( 'If you don\'t have an account, you can sign up for a free {name} email ' 'account at <a href="https://{url}">https://{url}</a>. {extra}') ).format(**service)) l.addWidget(self.tl, 0, 0, 3, 0) self.tl.setWordWrap(True) self.tl.setOpenExternalLinks(True) for name, label in ( ['from_', _('Your %s &email address:')], ['username', _('Your %s &username:'******'password', _('Your %s &password:'******'name']) le = QLineEdit(self) setattr(self, name, le) setattr(self, name + '_label', la) r = l.rowCount() l.addWidget(la, r, 0) l.addWidget(le, r, 1) la.setBuddy(le) if name == 'password': self.ptoggle = QCheckBox(_('&Show password'), self) l.addWidget(self.ptoggle, r, 2) self.ptoggle.stateChanged.connect( lambda s: self.password.setEchoMode( QLineEdit.EchoMode.Normal if s == Qt.CheckState.Checked else QLineEdit.EchoMode.Password)) self.username.setText(service['username']) self.password.setEchoMode(QLineEdit.EchoMode.Password) self.bl = QLabel('<p>' + _( 'If you plan to use email to send books to your Kindle, remember to' ' add your %s email address to the allowed email addresses in your ' 'Amazon.com Kindle management page.') % service['name']) self.bl.setWordWrap(True) l.addWidget(self.bl, l.rowCount(), 0, 3, 0) l.addWidget(bb, l.rowCount(), 0, 3, 0) self.setWindowTitle(_('Setup') + ' ' + service['name']) self.resize(self.sizeHint()) self.service = service def accept(self): un = unicode_type(self.username.text()) if self.service.get('at_in_username', False) and '@' not in un: return error_dialog( self, _('Incorrect username'), _('%s needs the full email address as your username') % self.service['name'], show=True) QDialog.accept(self)
class UnpackBook(QDialog): def __init__(self, parent, book_id, fmts, db): QDialog.__init__(self, parent) self.setWindowIcon(QIcon(I('unpack-book.png'))) self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db) self._exploded = None self._cleanup_dirs = [] self._cleanup_files = [] self.setup_ui() self.setWindowTitle(_('Unpack book') + ' - ' + db.title(book_id, index_is_id=True)) button = self.fmt_choice_buttons[0] button_map = {str(x.text()):x for x in self.fmt_choice_buttons} of = prefs['output_format'].upper() df = tweaks.get('default_tweak_format', None) lf = gprefs.get('last_tweak_format', None) if df and df.lower() == 'remember' and lf in button_map: button = button_map[lf] elif df and df.upper() in button_map: button = button_map[df.upper()] elif of in button_map: button = button_map[of] button.setChecked(True) self.init_state() for button in self.fmt_choice_buttons: button.toggled.connect(self.init_state) def init_state(self, *args): self._exploded = None self.preview_button.setEnabled(False) self.rebuild_button.setEnabled(False) self.explode_button.setEnabled(True) def setup_ui(self): # {{{ self._g = g = QHBoxLayout(self) self.setLayout(g) self._l = l = QVBoxLayout() g.addLayout(l) fmts = sorted(x.upper() for x in self.fmts) self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'), self) self._fl = fl = QHBoxLayout() self.fmt_choice_box.setLayout(self._fl) self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts] for x in self.fmt_choice_buttons: fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else 0) l.addWidget(self.fmt_choice_box) self.fmt_choice_box.setVisible(len(fmts) > 1) self.help_label = QLabel(_('''\ <h2>About Unpack book</h2> <p>Unpack book allows you to fine tune the appearance of an e-book by making small changes to its internals. In order to use Unpack book, you need to know a little bit about HTML and CSS, technologies that are used in e-books. Follow the steps:</p> <br> <ol> <li>Click "Explode book": This will "explode" the book into its individual internal components.<br></li> <li>Right click on any individual file and select "Open with..." to edit it in your favorite text editor.<br></li> <li>When you are done: <b>close the file browser window and the editor windows you used to make your tweaks</b>. Then click the "Rebuild book" button, to update the book in your calibre library.</li> </ol>''')) self.help_label.setWordWrap(True) self._fr = QFrame() self._fr.setFrameShape(QFrame.Shape.VLine) g.addWidget(self._fr) g.addWidget(self.help_label) self._b = b = QGridLayout() left, top, right, bottom = b.getContentsMargins() top += top b.setContentsMargins(left, top, right, bottom) l.addLayout(b, stretch=10) self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode book')) self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview book')) self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel')) self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild book')) self.explode_button.setToolTip( _('Explode the book to edit its components')) self.preview_button.setToolTip( _('Preview the result of your changes')) self.cancel_button.setToolTip( _('Abort without saving any changes')) self.rebuild_button.setToolTip( _('Save your changes and update the book in the calibre library')) a = b.addWidget a(self.explode_button, 0, 0, 1, 1) a(self.preview_button, 0, 1, 1, 1) a(self.cancel_button, 1, 0, 1, 1) a(self.rebuild_button, 1, 1, 1, 1) for x in ('explode', 'preview', 'cancel', 'rebuild'): getattr(self, x+'_button').clicked.connect(getattr(self, x)) self.msg = QLabel('dummy', self) self.msg.setVisible(False) self.msg.setStyleSheet(''' QLabel { text-align: center; background-color: white; color: black; border-width: 1px; border-style: solid; border-radius: 20px; font-size: x-large; font-weight: bold; } ''') self.resize(self.sizeHint() + QSize(40, 10)) # }}} def show_msg(self, msg): self.msg.setText(msg) self.msg.resize(self.size() - QSize(50, 25)) self.msg.move((self.width() - self.msg.width())//2, (self.height() - self.msg.height())//2) self.msg.setVisible(True) def hide_msg(self): self.msg.setVisible(False) def explode(self): self.show_msg(_('Exploding, please wait...')) if len(self.fmt_choice_buttons) > 1: gprefs.set('last_tweak_format', self.current_format.upper()) QTimer.singleShot(5, self.do_explode) def ask_question(self, msg): return question_dialog(self, _('Are you sure?'), msg) def do_explode(self): from calibre.ebooks.tweak import get_tools, Error, WorkerError tdir = PersistentTemporaryDirectory('_tweak_explode') self._cleanup_dirs.append(tdir) det_msg = None try: src = self.db.format(self.book_id, self.current_format, index_is_id=True, as_path=True) self._cleanup_files.append(src) exploder = get_tools(self.current_format)[0] opf = exploder(src, tdir, question=self.ask_question) except WorkerError as e: det_msg = e.orig_tb except Error as e: return error_dialog(self, _('Failed to unpack'), (_('Could not explode the %s file.')%self.current_format) + ' ' + as_unicode(e), show=True) except: import traceback det_msg = traceback.format_exc() finally: self.hide_msg() if det_msg is not None: return error_dialog(self, _('Failed to unpack'), _('Could not explode the %s file. Click "Show details" for ' 'more information.')%self.current_format, det_msg=det_msg, show=True) if opf is None: # The question was answered with No return self._exploded = tdir self.explode_button.setEnabled(False) self.preview_button.setEnabled(True) self.rebuild_button.setEnabled(True) open_local_file(tdir) def rebuild_it(self): from calibre.ebooks.tweak import get_tools, WorkerError src_dir = self._exploded det_msg = None of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower()) of.close() of = of.name self._cleanup_files.append(of) try: rebuilder = get_tools(self.current_format)[1] rebuilder(src_dir, of) except WorkerError as e: det_msg = e.orig_tb except: import traceback det_msg = traceback.format_exc() finally: self.hide_msg() if det_msg is not None: error_dialog(self, _('Failed to rebuild file'), _('Failed to rebuild %s. For more information, click ' '"Show details".')%self.current_format, det_msg=det_msg, show=True) return None return of def preview(self): self.show_msg(_('Rebuilding, please wait...')) QTimer.singleShot(5, self.do_preview) def do_preview(self): rebuilt = self.rebuild_it() if rebuilt is not None: self.parent().iactions['View']._view_file(rebuilt) def rebuild(self): self.show_msg(_('Rebuilding, please wait...')) QTimer.singleShot(5, self.do_rebuild) def do_rebuild(self): rebuilt = self.rebuild_it() if rebuilt is not None: fmt = os.path.splitext(rebuilt)[1][1:].upper() with open(rebuilt, 'rb') as f: self.db.add_format(self.book_id, fmt, f, index_is_id=True) self.accept() def cancel(self): self.reject() def cleanup(self): if ismacos and self._exploded: try: import appscript self.finder = appscript.app('Finder') self.finder.Finder_windows[os.path.basename(self._exploded)].close() except: pass for f in self._cleanup_files: try: os.remove(f) except: pass for d in self._cleanup_dirs: try: shutil.rmtree(d) except: pass @property def db(self): return self.db_ref() @property def current_format(self): for b in self.fmt_choice_buttons: if b.isChecked(): return str(b.text())
class ItemView(QStackedWidget): # {{{ add_new_item = pyqtSignal(object, object) delete_item = pyqtSignal() flatten_item = pyqtSignal() go_to_root = pyqtSignal() create_from_xpath = pyqtSignal(object, object) create_from_links = pyqtSignal() create_from_files = pyqtSignal() flatten_toc = pyqtSignal() def __init__(self, parent, prefs): QStackedWidget.__init__(self, parent) self.prefs = prefs self.setMinimumWidth(250) self.root_pane = rp = QWidget(self) self.item_pane = ip = QWidget(self) self.current_item = None sa = QScrollArea(self) sa.setWidgetResizable(True) sa.setWidget(rp) self.addWidget(sa) sa = QScrollArea(self) sa.setWidgetResizable(True) sa.setWidget(ip) self.addWidget(sa) self.l1 = la = QLabel('<p>' + _( 'You can edit existing entries in the Table of Contents by clicking them' ' in the panel to the left.' ) + '<p>' + _( 'Entries with a green tick next to them point to a location that has ' 'been verified to exist. Entries with a red dot are broken and may need' ' to be fixed.')) la.setStyleSheet('QLabel { margin-bottom: 20px }') la.setWordWrap(True) l = rp.l = QVBoxLayout() rp.setLayout(l) l.addWidget(la) self.add_new_to_root_button = b = QPushButton(_('Create a &new entry')) b.clicked.connect(self.add_new_to_root) l.addWidget(b) l.addStretch() self.cfmhb = b = QPushButton(_('Generate ToC from &major headings')) b.clicked.connect(self.create_from_major_headings) b.setToolTip( textwrap.fill( _('Generate a Table of Contents from the major headings in the book.' ' This will work if the book identifies its headings using HTML' ' heading tags. Uses the <h1>, <h2> and <h3> tags.'))) l.addWidget(b) self.cfmab = b = QPushButton(_('Generate ToC from &all headings')) b.clicked.connect(self.create_from_all_headings) b.setToolTip( textwrap.fill( _('Generate a Table of Contents from all the headings in the book.' ' This will work if the book identifies its headings using HTML' ' heading tags. Uses the <h1-6> tags.'))) l.addWidget(b) self.lb = b = QPushButton(_('Generate ToC from &links')) b.clicked.connect(self.create_from_links) b.setToolTip( textwrap.fill( _('Generate a Table of Contents from all the links in the book.' ' Links that point to destinations that do not exist in the book are' ' ignored. Also multiple links with the same destination or the same' ' text are ignored.'))) l.addWidget(b) self.cfb = b = QPushButton(_('Generate ToC from &files')) b.clicked.connect(self.create_from_files) b.setToolTip( textwrap.fill( _('Generate a Table of Contents from individual files in the book.' ' Each entry in the ToC will point to the start of the file, the' ' text of the entry will be the "first line" of text from the file.' ))) l.addWidget(b) self.xpb = b = QPushButton(_('Generate ToC from &XPath')) b.clicked.connect(self.create_from_user_xpath) b.setToolTip( textwrap.fill( _('Generate a Table of Contents from arbitrary XPath expressions.' ))) l.addWidget(b) self.fal = b = QPushButton(_('&Flatten the ToC')) b.clicked.connect(self.flatten_toc) b.setToolTip( textwrap.fill( _('Flatten the Table of Contents, putting all entries at the top level' ))) l.addWidget(b) l.addStretch() self.w1 = la = QLabel( _('<b>WARNING:</b> calibre only supports the ' 'creation of linear ToCs in AZW3 files. In a ' 'linear ToC every entry must point to a ' 'location after the previous entry. If you ' 'create a non-linear ToC it will be ' 'automatically re-arranged inside the AZW3 file.')) la.setWordWrap(True) l.addWidget(la) l = ip.l = QGridLayout() ip.setLayout(l) la = ip.heading = QLabel('') l.addWidget(la, 0, 0, 1, 2) la.setWordWrap(True) la = ip.la = QLabel( _('You can move this entry around the Table of Contents by drag ' 'and drop or using the up and down buttons to the left')) la.setWordWrap(True) l.addWidget(la, 1, 0, 1, 2) # Item status ip.hl1 = hl = QFrame() hl.setFrameShape(QFrame.Shape.HLine) l.addWidget(hl, l.rowCount(), 0, 1, 2) self.icon_label = QLabel() self.status_label = QLabel() self.status_label.setWordWrap(True) l.addWidget(self.icon_label, l.rowCount(), 0) l.addWidget(self.status_label, l.rowCount() - 1, 1) ip.hl2 = hl = QFrame() hl.setFrameShape(QFrame.Shape.HLine) l.addWidget(hl, l.rowCount(), 0, 1, 2) # Edit/remove item rs = l.rowCount() ip.b1 = b = QPushButton(QIcon(I('edit_input.png')), _('Change the &location this entry points to'), self) b.clicked.connect(self.edit_item) l.addWidget(b, l.rowCount() + 1, 0, 1, 2) ip.b2 = b = QPushButton(QIcon(I('trash.png')), _('&Remove this entry'), self) l.addWidget(b, l.rowCount(), 0, 1, 2) b.clicked.connect(self.delete_item) ip.hl3 = hl = QFrame() hl.setFrameShape(QFrame.Shape.HLine) l.addWidget(hl, l.rowCount(), 0, 1, 2) l.setRowMinimumHeight(rs, 20) # Add new item rs = l.rowCount() ip.b3 = b = QPushButton(QIcon(I('plus.png')), _('New entry &inside this entry')) connect_lambda(b.clicked, self, lambda self: self.add_new('inside')) l.addWidget(b, l.rowCount() + 1, 0, 1, 2) ip.b4 = b = QPushButton(QIcon(I('plus.png')), _('New entry &above this entry')) connect_lambda(b.clicked, self, lambda self: self.add_new('before')) l.addWidget(b, l.rowCount(), 0, 1, 2) ip.b5 = b = QPushButton(QIcon(I('plus.png')), _('New entry &below this entry')) connect_lambda(b.clicked, self, lambda self: self.add_new('after')) l.addWidget(b, l.rowCount(), 0, 1, 2) # Flatten entry ip.b3 = b = QPushButton(QIcon(I('heuristics.png')), _('&Flatten this entry')) b.clicked.connect(self.flatten_item) b.setToolTip( _('All children of this entry are brought to the same ' 'level as this entry.')) l.addWidget(b, l.rowCount() + 1, 0, 1, 2) ip.hl4 = hl = QFrame() hl.setFrameShape(QFrame.Shape.HLine) l.addWidget(hl, l.rowCount(), 0, 1, 2) l.setRowMinimumHeight(rs, 20) # Return to welcome rs = l.rowCount() ip.b4 = b = QPushButton(QIcon(I('back.png')), _('&Return to welcome screen')) b.clicked.connect(self.go_to_root) b.setToolTip(_('Go back to the top level view')) l.addWidget(b, l.rowCount() + 1, 0, 1, 2) l.setRowMinimumHeight(rs, 20) l.addWidget(QLabel(), l.rowCount(), 0, 1, 2) l.setColumnStretch(1, 10) l.setRowStretch(l.rowCount() - 1, 10) self.w2 = la = QLabel(self.w1.text()) self.w2.setWordWrap(True) l.addWidget(la, l.rowCount(), 0, 1, 2) def ask_if_duplicates_should_be_removed(self): return not question_dialog( self, _('Remove duplicates'), _('Should headings with the same text at the same level be included?' ), yes_text=_('&Include duplicates'), no_text=_('&Remove duplicates')) def create_from_major_headings(self): self.create_from_xpath.emit(['//h:h%d' % i for i in range(1, 4)], self.ask_if_duplicates_should_be_removed()) def create_from_all_headings(self): self.create_from_xpath.emit(['//h:h%d' % i for i in range(1, 7)], self.ask_if_duplicates_should_be_removed()) def create_from_user_xpath(self): d = XPathDialog(self, self.prefs) if d.exec_() == QDialog.DialogCode.Accepted and d.xpaths: self.create_from_xpath.emit(d.xpaths, d.remove_duplicates_cb.isChecked()) def hide_azw3_warning(self): self.w1.setVisible(False), self.w2.setVisible(False) def add_new_to_root(self): self.add_new_item.emit(None, None) def add_new(self, where): self.add_new_item.emit(self.current_item, where) def edit_item(self): self.add_new_item.emit(self.current_item, None) def __call__(self, item): if item is None: self.current_item = None self.setCurrentIndex(0) else: self.current_item = item self.setCurrentIndex(1) self.populate_item_pane() def populate_item_pane(self): item = self.current_item name = unicode_type(item.data(0, Qt.ItemDataRole.DisplayRole) or '') self.item_pane.heading.setText('<h2>%s</h2>' % name) self.icon_label.setPixmap( item.data(0, Qt.ItemDataRole.DecorationRole).pixmap(32, 32)) tt = _('This entry points to an existing destination') toc = item.data(0, Qt.ItemDataRole.UserRole) if toc.dest_exists is False: tt = _('The location this entry points to does not exist') elif toc.dest_exists is None: tt = '' self.status_label.setText(tt) def data_changed(self, item): if item is self.current_item: self.populate_item_pane()
class DownloadDialog(QDialog): # {{{ def __init__(self, url, fname, parent): QDialog.__init__(self, parent) self.setWindowTitle(_('Download %s')%fname) self.l = QVBoxLayout(self) self.purl = urlparse(url) self.msg = QLabel(_('Downloading <b>%(fname)s</b> from %(url)s')%dict( fname=fname, url=self.purl.netloc)) self.msg.setWordWrap(True) self.l.addWidget(self.msg) self.pb = QProgressBar(self) self.pb.setMinimum(0) self.pb.setMaximum(0) self.l.addWidget(self.pb) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel, Qt.Orientation.Horizontal, self) self.l.addWidget(self.bb) self.bb.rejected.connect(self.reject) sz = self.sizeHint() self.resize(max(sz.width(), 400), sz.height()) fpath = PersistentTemporaryFile(os.path.splitext(fname)[1]) fpath.close() self.fpath = fpath.name self.worker = Worker(url, self.fpath, Queue()) self.rejected = False def reject(self): self.rejected = True QDialog.reject(self) def start_download(self): self.worker.start() QTimer.singleShot(50, self.update) self.exec_() if self.worker.err is not None: error_dialog(self.parent(), _('Download failed'), _('Failed to download from %(url)r with error: %(err)s')%dict( url=self.worker.url, err=self.worker.err), det_msg=self.worker.tb, show=True) def update(self): if self.rejected: return try: progress = self.worker.rq.get_nowait() except Empty: pass else: self.update_pb(progress) if not self.worker.is_alive(): return self.accept() QTimer.singleShot(50, self.update) def update_pb(self, progress): transferred, block_size, total = progress if total == -1: self.pb.setMaximum(0) self.pb.setMinimum(0) self.pb.setValue(0) else: so_far = transferred * block_size self.pb.setMaximum(max(total, so_far)) self.pb.setValue(so_far) @property def err(self): return self.worker.err