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)
class TitleBar(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QHBoxLayout(self) self.icon = Icon(self, size=ICON_SIZE) l.addWidget(self.icon) self.title = QLabel('') self.title.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) l.addWidget(self.title) l.addStrut(25) self.msg = la = Message(self) l.addWidget(la) self.default_message = __appname__ + ' ' + _('version') + ' ' + \ __version__ + ' ' + _('created by Kovid Goyal') self.show_plugin() self.show_msg() def show_plugin(self, plugin=None): self.icon.set_icon( QIcon(I('lt.png') if plugin is None else plugin.icon)) self.title.setText( '<h1>' + (_('Preferences') if plugin is None else plugin.gui_name)) def show_msg(self, msg=None): msg = msg or self.default_message self.msg.setText(' '.join(msg.splitlines()).strip())
class CursorPositionWidget(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) self.l = QHBoxLayout(self) self.setLayout(self.l) self.la = QLabel('') self.l.addWidget(self.la) self.l.setContentsMargins(0, 0, 0, 0) f = self.la.font() f.setBold(False) self.la.setFont(f) def update_position(self, line=None, col=None, character=None): if line is None: self.la.setText('') else: try: name = character_name_from_code(ord_string(character)[0]) if character and tprefs['editor_show_char_under_cursor'] else None except Exception: name = None text = _('Line: {0} : {1}').format(line, col) if not name: name = {'\t':'TAB'}.get(character, None) if name and tprefs['editor_show_char_under_cursor']: text = name + ' : ' + text self.la.setText(text)
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()
class ChooseName(Dialog): # {{{ ''' Chooses the filename for a newly imported file, with error checking ''' def __init__(self, candidate, parent=None): self.candidate = candidate self.filename = None Dialog.__init__(self, _('Choose file name'), 'choose-file-name', parent=parent) def setup_ui(self): self.l = l = QFormLayout(self) self.setLayout(l) self.err_label = QLabel('') self.name_edit = QLineEdit(self) self.name_edit.textChanged.connect(self.verify) self.name_edit.setText(self.candidate) pos = self.candidate.rfind('.') if pos > -1: self.name_edit.setSelection(0, pos) l.addRow(_('File &name:'), self.name_edit) l.addRow(self.err_label) l.addRow(self.bb) def show_error(self, msg): self.err_label.setText('<p style="color:red">' + msg) return False def verify(self): return name_is_ok(str(self.name_edit.text()), self.show_error) def accept(self): if not self.verify(): return error_dialog( self, _('No name specified'), _('You must specify a file name for the new file, with an extension.' ), show=True) n = str(self.name_edit.text()).replace('\\', '/') name, ext = n.rpartition('.')[0::2] self.filename = name + '.' + ext.lower() super().accept()
class CoverZoom(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) self.l = l = QVBoxLayout(self) self.cover = ZoomedCover(self) l.addWidget(self.cover) self.h = QHBoxLayout() l.addLayout(self.h) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, self) self.size_label = QLabel(self) self.h.addWidget(self.size_label) self.h.addStretch(10) self.h.addWidget(self.bb) def set_pixmap(self, pixmap): self.cover.pixmap = pixmap self.size_label.setText( _('Cover size: {0}x{1}').format(pixmap.width(), pixmap.height())) self.cover.update()
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 WaitPanel(QWidget): def __init__(self, msg, parent=None, size=256, interval=10): QWidget.__init__(self, parent) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.l = l = QVBoxLayout(self) self.spinner = ProgressIndicator(self, size, interval) self.start, self.stop = self.spinner.start, self.spinner.stop l.addStretch(), l.addWidget(self.spinner, 0, Qt.AlignmentFlag.AlignCenter) self.la = QLabel(msg) f = self.la.font() f.setPointSize(28) self.la.setFont(f) l.addWidget(self.la, 0, Qt.AlignmentFlag.AlignCenter), l.addStretch() @property def msg(self): return self.la.text() @msg.setter def msg(self, val): self.la.setText(val)
def test(): from qt.core import QApplication, QLabel, QTimer app = QApplication([]) l = QLabel() l.setText('Waiting for message...') def show_message(msg): print(msg) l.setText(msg.decode('utf-8')) def send(): send_message_via_worker('hello!', wait_till_sent=False) QTimer.singleShot(1000, send) s = Listener(parent=l) s.start_listening() print('Listening at:', s.serverName(), s.isListening()) s.message_received.connect(show_message) l.show() app.exec() del app
class SaveWidget(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QHBoxLayout(self) self.setLayout(l) self.label = QLabel('') self.pi = ProgressIndicator(self, 24) l.addWidget(self.label) l.addWidget(self.pi) l.setContentsMargins(0, 0, 0, 0) self.pi.setVisible(False) self.stop() def start(self): self.pi.setDisplaySize(QSize(self.label.height(), self.label.height())) self.pi.setVisible(True) self.pi.startAnimation() self.label.setText(_('Saving...')) def stop(self): self.pi.setVisible(False) self.pi.stopAnimation() self.label.setText('')
class Config(QDialog): ''' Configuration dialog for single book conversion. If accepted, has the following important attributes output_format - Output format (without a leading .) input_format - Input format (without a leading .) opf_path - Path to OPF file with user specified metadata cover_path - Path to user specified cover (can be None) recommendations - A pickled list of 3 tuples in the same format as the recommendations member of the Input/Output plugins. ''' def __init__(self, parent, db, book_id, preferred_input_format=None, preferred_output_format=None): QDialog.__init__(self, parent) self.widgets = [] self.setupUi() self.opt_individual_saved_settings.setVisible(False) self.db, self.book_id = db, book_id self.setup_input_output_formats(self.db, self.book_id, preferred_input_format, preferred_output_format) self.setup_pipeline() self.input_formats.currentIndexChanged[native_string_type].connect( self.setup_pipeline) self.output_formats.currentIndexChanged[native_string_type].connect( self.setup_pipeline) self.groups.setSpacing(5) self.groups.entered[(QModelIndex)].connect(self.show_group_help) rb = self.buttonBox.button( QDialogButtonBox.StandardButton.RestoreDefaults) rb.setText(_('Restore &defaults')) rb.setIcon(QIcon(I('clear_left.png'))) rb.clicked.connect(self.restore_defaults) self.groups.setMouseTracking(True) geom = gprefs.get('convert_single_dialog_geom', None) if geom: QApplication.instance().safe_restore_geometry(self, geom) else: self.resize(self.sizeHint()) def current_group_changed(self, cur, prev): self.show_pane(cur) def setupUi(self): self.setObjectName("Dialog") self.resize(1024, 700) self.setWindowIcon(QIcon(I('convert.png'))) self.gridLayout = QGridLayout(self) self.gridLayout.setObjectName("gridLayout") self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.input_label = QLabel(self) self.input_label.setObjectName("input_label") self.horizontalLayout.addWidget(self.input_label) self.input_formats = QComboBox(self) self.input_formats.setSizeAdjustPolicy( QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) self.input_formats.setMinimumContentsLength(5) self.input_formats.setObjectName("input_formats") self.horizontalLayout.addWidget(self.input_formats) self.opt_individual_saved_settings = QCheckBox(self) self.opt_individual_saved_settings.setObjectName( "opt_individual_saved_settings") self.horizontalLayout.addWidget(self.opt_individual_saved_settings) spacerItem = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout.addItem(spacerItem) self.label_2 = QLabel(self) self.label_2.setObjectName("label_2") self.horizontalLayout.addWidget(self.label_2) self.output_formats = QComboBox(self) self.output_formats.setSizeAdjustPolicy( QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) self.output_formats.setMinimumContentsLength(5) self.output_formats.setObjectName("output_formats") self.horizontalLayout.addWidget(self.output_formats) self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 2) self.groups = QListView(self) sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.groups.sizePolicy().hasHeightForWidth()) self.groups.setSizePolicy(sizePolicy) self.groups.setTabKeyNavigation(True) self.groups.setIconSize(QSize(48, 48)) self.groups.setWordWrap(True) self.groups.setObjectName("groups") self.gridLayout.addWidget(self.groups, 1, 0, 3, 1) self.scrollArea = QScrollArea(self) sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(4) sizePolicy.setVerticalStretch(10) sizePolicy.setHeightForWidth( self.scrollArea.sizePolicy().hasHeightForWidth()) self.scrollArea.setSizePolicy(sizePolicy) self.scrollArea.setFrameShape(QFrame.Shape.NoFrame) self.scrollArea.setLineWidth(0) self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.page = QWidget() self.page.setObjectName("page") self.gridLayout.addWidget(self.scrollArea, 1, 1, 1, 1) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Orientation.Horizontal) self.buttonBox.setStandardButtons( QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.RestoreDefaults) self.buttonBox.setObjectName("buttonBox") self.gridLayout.addWidget(self.buttonBox, 3, 1, 1, 1) self.help = QTextEdit(self) self.help.setReadOnly(True) sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.help.sizePolicy().hasHeightForWidth()) self.help.setSizePolicy(sizePolicy) self.help.setMaximumHeight(80) self.help.setObjectName("help") self.gridLayout.addWidget(self.help, 2, 1, 1, 1) self.input_label.setBuddy(self.input_formats) self.label_2.setBuddy(self.output_formats) self.input_label.setText(_("&Input format:")) self.opt_individual_saved_settings.setText( _("Use &saved conversion settings for individual books")) self.label_2.setText(_("&Output format:")) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) def sizeHint(self): geom = self.screen().availableSize() nh, nw = max(300, geom.height() - 100), max(400, geom.width() - 70) return QSize(nw, nh) def restore_defaults(self): delete_specifics(self.db, self.book_id) self.setup_pipeline() @property def input_format(self): return str(self.input_formats.currentText()).lower() @property def output_format(self): return str(self.output_formats.currentText()).lower() @property def manually_fine_tune_toc(self): for w in self.widgets: if hasattr(w, 'manually_fine_tune_toc'): return w.manually_fine_tune_toc.isChecked() def setup_pipeline(self, *args): oidx = self.groups.currentIndex().row() input_format = self.input_format output_format = self.output_format self.plumber = create_dummy_plumber(input_format, output_format) def widget_factory(cls): return cls(self, self.plumber.get_option_by_name, self.plumber.get_option_help, self.db, self.book_id) self.mw = widget_factory(MetadataWidget) self.setWindowTitle(_('Convert') + ' ' + str(self.mw.title.text())) lf = widget_factory(LookAndFeelWidget) hw = widget_factory(HeuristicsWidget) sr = widget_factory(SearchAndReplaceWidget) ps = widget_factory(PageSetupWidget) sd = widget_factory(StructureDetectionWidget) toc = widget_factory(TOCWidget) from calibre.gui2.actions.toc_edit import SUPPORTED toc.manually_fine_tune_toc.setVisible( output_format.upper() in SUPPORTED) debug = widget_factory(DebugWidget) output_widget = self.plumber.output_plugin.gui_configuration_widget( self, self.plumber.get_option_by_name, self.plumber.get_option_help, self.db, self.book_id) input_widget = self.plumber.input_plugin.gui_configuration_widget( self, self.plumber.get_option_by_name, self.plumber.get_option_help, self.db, self.book_id) self.break_cycles() self.widgets = widgets = [self.mw, lf, hw, ps, sd, toc, sr] if input_widget is not None: widgets.append(input_widget) if output_widget is not None: widgets.append(output_widget) widgets.append(debug) for w in widgets: w.set_help_signal.connect(self.help.setPlainText) w.setVisible(False) self._groups_model = GroupModel(widgets) self.groups.setModel(self._groups_model) idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0 self.groups.setCurrentIndex(self._groups_model.index(idx)) self.show_pane(idx) self.groups.selectionModel().currentChanged.connect( self.current_group_changed) try: shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True) except Exception: pass def setup_input_output_formats(self, db, book_id, preferred_input_format, preferred_output_format): if preferred_output_format: preferred_output_format = preferred_output_format.upper() output_formats = get_output_formats(preferred_output_format) input_format, input_formats = get_input_format_for_book( db, book_id, preferred_input_format) preferred_output_format = preferred_output_format if \ preferred_output_format in output_formats else \ sort_formats_by_preference(output_formats, [prefs['output_format']])[0] self.input_formats.addItems(str(x.upper()) for x in input_formats) self.output_formats.addItems(str(x.upper()) for x in output_formats) self.input_formats.setCurrentIndex(input_formats.index(input_format)) self.output_formats.setCurrentIndex( output_formats.index(preferred_output_format)) def show_pane(self, index): if hasattr(index, 'row'): index = index.row() ow = self.scrollArea.takeWidget() if ow: ow.setParent(self) for i, w in enumerate(self.widgets): if i == index: self.scrollArea.setWidget(w) w.show() else: w.setVisible(False) def accept(self): recs = GuiRecommendations() for w in self._groups_model.widgets: if not w.pre_commit_check(): return x = w.commit(save_defaults=False) recs.update(x) self.opf_file, self.cover_file = self.mw.opf_file, self.mw.cover_file self._recommendations = recs if self.db is not None: recs['gui_preferred_input_format'] = self.input_format save_specifics(self.db, self.book_id, recs) self.break_cycles() QDialog.accept(self) def reject(self): self.break_cycles() QDialog.reject(self) def done(self, r): if self.isVisible(): gprefs['convert_single_dialog_geom'] = \ bytearray(self.saveGeometry()) return QDialog.done(self, r) def break_cycles(self): for w in self.widgets: w.break_cycles() @property def recommendations(self): recs = [(k, v, OptionRecommendation.HIGH) for k, v in self._recommendations.items()] return recs def show_group_help(self, index): widget = self._groups_model.widgets[index.row()] self.help.setPlainText(widget.HELP)
class CoversWidget(QWidget): # {{{ chosen = pyqtSignal() finished = pyqtSignal() def __init__(self, log, current_cover, parent=None): QWidget.__init__(self, parent) self.log = log self.abort = Event() self.l = l = QGridLayout() self.setLayout(l) self.msg = QLabel() self.msg.setWordWrap(True) l.addWidget(self.msg, 0, 0) self.covers_view = CoversView(current_cover, self) self.covers_view.chosen.connect(self.chosen) l.addWidget(self.covers_view, 1, 0) self.continue_processing = True def reset_covers(self): self.covers_view.reset_covers() def start(self, book, current_cover, title, authors, caches): self.continue_processing = True self.abort.clear() self.book, self.current_cover = book, current_cover self.title, self.authors = title, authors self.log('Starting cover download for:', book.title) self.log('Query:', title, authors, self.book.identifiers) self.msg.setText( '<p>' + _('Downloading covers for <b>%s</b>, please wait...') % book.title) self.covers_view.start() self.worker = CoverWorker(self.log, self.abort, self.title, self.authors, book.identifiers, caches) self.worker.start() QTimer.singleShot(50, self.check) self.covers_view.setFocus(Qt.FocusReason.OtherFocusReason) def check(self): if self.worker.is_alive() and not self.abort.is_set(): QTimer.singleShot(50, self.check) try: self.process_result(self.worker.rq.get_nowait()) except Empty: pass else: self.process_results() def process_results(self): while self.continue_processing: try: self.process_result(self.worker.rq.get_nowait()) except Empty: break if self.continue_processing: self.covers_view.clear_failed() if self.worker.error and self.worker.error.strip(): error_dialog(self, _('Download failed'), _('Failed to download any covers, click' ' "Show details" for details.'), det_msg=self.worker.error, show=True) num = self.covers_view.model().rowCount() if num < 2: txt = _( 'Could not find any covers for <b>%s</b>') % self.book.title else: if num == 2: txt = _('Found a cover for {title}').format(title=self.title) else: txt = _( 'Found <b>{num}</b> covers for {title}. When the download completes,' ' the covers will be sorted by size.').format( title=self.title, num=num - 1) self.msg.setText(txt) self.msg.setWordWrap(True) self.covers_view.stop() self.finished.emit() def process_result(self, result): if not self.continue_processing: return plugin_name, width, height, fmt, data = result self.covers_view.model().update_result(plugin_name, width, height, data) def cleanup(self): self.covers_view.delegate.stop_animation() self.continue_processing = False def cancel(self): self.cleanup() self.abort.set() def cover_pixmap(self): idx = None for i in self.covers_view.selectionModel().selectedIndexes(): if i.isValid(): idx = i break if idx is None: idx = self.covers_view.currentIndex() return self.covers_view.model().cover_pixmap(idx)
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 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())
def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter, ttm_is_first_letter=False, category=None, fm=None): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) self.verticalLayout_2.setAlignment(Qt.AlignmentFlag.AlignCenter) self.search_box.setMinimumContentsLength(25) # Put the category name into the title bar t = self.windowTitle() self.category_name = cat_name self.category = category self.setWindowTitle(t + ' (' + cat_name + ')') # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags() & (~Qt.WindowType.WindowContextHelpButtonHint)) self.setWindowIcon(icon) # Get saved geometry info try: self.table_column_widths = \ gprefs.get('tag_list_editor_table_widths', None) except: pass # initialization self.to_rename = {} self.to_delete = set() self.all_tags = {} self.original_names = {} self.ordered_tags = [] self.sorter = sorter self.get_book_ids = get_book_ids self.text_before_editing = '' # Capture clicks on the horizontal header to sort the table columns hh = self.table.horizontalHeader() hh.sectionResized.connect(self.table_column_resized) hh.setSectionsClickable(True) hh.sectionClicked.connect(self.do_sort) hh.setSortIndicatorShown(True) self.last_sorted_by = 'name' self.name_order = 0 self.count_order = 1 self.was_order = 1 self.edit_delegate = EditColumnDelegate(self.table) self.edit_delegate.editing_finished.connect(self.stop_editing) self.edit_delegate.editing_started.connect(self.start_editing) self.table.setItemDelegateForColumn(0, self.edit_delegate) if prefs['case_sensitive']: self.string_contains = contains else: self.string_contains = self.case_insensitive_compare self.delete_button.clicked.connect(self.delete_tags) self.table.delete_pressed.connect(self.delete_pressed) self.rename_button.clicked.connect(self.rename_tag) self.undo_button.clicked.connect(self.undo_edit) self.table.itemDoubleClicked.connect(self._rename_tag) self.table.itemChanged.connect(self.finish_editing) self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText( _('&OK')) self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText( _('&Cancel')) self.buttonBox.accepted.connect(self.accepted) self.search_box.initialize('tag_list_search_box_' + cat_name) le = self.search_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_search) self.search_box.textChanged.connect(self.search_text_changed) self.search_button.clicked.connect(self.do_search) self.search_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.Shape.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignmentFlag.AlignVCenter) l.resize(l.sizeHint()) l.move(10, 0) 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.filter_box.initialize('tag_list_filter_box_' + cat_name) le = self.filter_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_filter) le.returnPressed.connect(self.do_filter) self.filter_button.clicked.connect(self.do_filter) self.apply_vl_checkbox.clicked.connect(self.vl_box_changed) self.table.setEditTriggers( QAbstractItemView.EditTrigger.EditKeyPressed) try: geom = gprefs.get('tag_list_editor_dialog_geometry', None) if geom is not None: QApplication.instance().safe_restore_geometry( self, QByteArray(geom)) else: self.resize(self.sizeHint() + QSize(150, 100)) except: pass self.is_enumerated = False if fm: if fm['datatype'] == 'enumeration': self.is_enumerated = True self.enum_permitted_values = fm.get('display', {}).get( 'enum_values', None) # Add the data self.search_item_row = -1 self.fill_in_table(None, tag_to_match, ttm_is_first_letter) self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu)
class Diff(Dialog): revert_requested = pyqtSignal() line_activated = pyqtSignal(object, object, object) def __init__(self, revert_button_msg=None, parent=None, show_open_in_editor=False, show_as_window=False): self.context = 3 self.beautify = False self.apply_diff_calls = [] self.show_open_in_editor = show_open_in_editor self.revert_button_msg = revert_button_msg Dialog.__init__(self, _('Differences between books'), 'diff-dialog', parent=parent) self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMinMaxButtonsHint) if show_as_window: self.setWindowFlags(Qt.WindowType.Window) self.view.line_activated.connect(self.line_activated) def sizeHint(self): geom = self.screen().availableSize() return QSize(int(0.9 * geom.width()), int(0.8 * geom.height())) def setup_ui(self): self.setWindowIcon(QIcon(I('diff.png'))) self.stacks = st = QStackedLayout(self) self.busy = BusyWidget(self) self.w = QWidget(self) st.addWidget(self.busy), st.addWidget(self.w) self.setLayout(st) self.l = l = QGridLayout() self.w.setLayout(l) self.view = v = DiffView(self, show_open_in_editor=self.show_open_in_editor) l.addWidget(v, l.rowCount(), 0, 1, -1) r = l.rowCount() self.bp = b = QToolButton(self) b.setIcon(QIcon(I('back.png'))) connect_lambda(b.clicked, self, lambda self: self.view.next_change(-1)) b.setToolTip(_('Go to previous change') + ' [p]') b.setText(_('&Previous change')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) l.addWidget(b, r, 0) self.bn = b = QToolButton(self) b.setIcon(QIcon(I('forward.png'))) connect_lambda(b.clicked, self, lambda self: self.view.next_change(1)) b.setToolTip(_('Go to next change') + ' [n]') b.setText(_('&Next change')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) l.addWidget(b, r, 1) self.search = s = HistoryLineEdit2(self) s.initialize('diff_search_history') l.addWidget(s, r, 2) s.setPlaceholderText(_('Search for text')) connect_lambda(s.returnPressed, self, lambda self: self.do_search(False)) self.sbn = b = QToolButton(self) b.setIcon(QIcon(I('arrow-down.png'))) connect_lambda(b.clicked, self, lambda self: self.do_search(False)) b.setToolTip(_('Find next match')) b.setText(_('Next &match')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) l.addWidget(b, r, 3) self.sbp = b = QToolButton(self) b.setIcon(QIcon(I('arrow-up.png'))) connect_lambda(b.clicked, self, lambda self: self.do_search(True)) b.setToolTip(_('Find previous match')) b.setText(_('P&revious match')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) l.addWidget(b, r, 4) self.lb = b = QRadioButton(_('Left panel'), self) b.setToolTip(_('Perform search in the left panel')) l.addWidget(b, r, 5) self.rb = b = QRadioButton(_('Right panel'), self) b.setToolTip(_('Perform search in the right panel')) l.addWidget(b, r, 6) b.setChecked(True) self.pb = b = QToolButton(self) b.setIcon(QIcon(I('config.png'))) b.setText(_('&Options')), b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) b.setToolTip(_('Change how the differences are displayed')) b.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) m = QMenu(b) b.setMenu(m) cm = self.cm = QMenu(_('Lines of context around each change')) for i in (3, 5, 10, 50): cm.addAction(_('Show %d lines of context') % i, partial(self.change_context, i)) cm.addAction(_('Show all text'), partial(self.change_context, None)) self.beautify_action = m.addAction('', self.toggle_beautify) self.set_beautify_action_text() m.addMenu(cm) l.addWidget(b, r, 7) self.hl = QHBoxLayout() l.addLayout(self.hl, l.rowCount(), 0, 1, -1) self.names = QLabel('') self.hl.addWidget(self.names, stretch=100) if self.show_open_in_editor: self.edit_msg = QLabel(_('Double click right side to edit')) self.edit_msg.setToolTip(textwrap.fill(_( 'Double click on any change in the right panel to edit that location in the editor'))) self.hl.addWidget(self.edit_msg) self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Close) if self.revert_button_msg is not None: self.rvb = b = self.bb.addButton(self.revert_button_msg, QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon(I('edit-undo.png'))), b.setAutoDefault(False) b.clicked.connect(self.revert_requested) b.clicked.connect(self.reject) self.bb.button(QDialogButtonBox.StandardButton.Close).setDefault(True) self.hl.addWidget(self.bb) self.view.setFocus(Qt.FocusReason.OtherFocusReason) def break_cycles(self): self.view = None for x in ('revert_requested', 'line_activated'): try: getattr(self, x).disconnect() except: pass def do_search(self, reverse): text = str(self.search.text()) if not text.strip(): return v = self.view.view.left if self.lb.isChecked() else self.view.view.right v.search(text, reverse=reverse) def change_context(self, context): if context == self.context: return self.context = context self.refresh() def refresh(self): with self: self.view.clear() for args, kwargs in self.apply_diff_calls: kwargs['context'] = self.context kwargs['beautify'] = self.beautify self.view.add_diff(*args, **kwargs) self.view.finalize() def toggle_beautify(self): self.beautify = not self.beautify self.set_beautify_action_text() self.refresh() def set_beautify_action_text(self): self.beautify_action.setText( _('Beautify files before comparing them') if not self.beautify else _('Do not beautify files before comparing')) def __enter__(self): self.stacks.setCurrentIndex(0) self.busy.setVisible(True) self.busy.pi.startAnimation() QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) QApplication.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents | QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers) def __exit__(self, *args): self.busy.pi.stopAnimation() self.stacks.setCurrentIndex(1) QApplication.restoreOverrideCursor() def set_names(self, names): t = '' if isinstance(names, tuple): t = '%s <--> %s' % names self.names.setText(t) def ebook_diff(self, path1, path2, names=None): self.set_names(names) with self: identical = self.apply_diff(_('The books are identical'), *ebook_diff(path1, path2)) self.view.finalize() if identical: self.reject() def container_diff(self, left, right, identical_msg=None, names=None): self.set_names(names) with self: identical = self.apply_diff(identical_msg or _('No changes found'), *container_diff(left, right)) self.view.finalize() if identical: self.reject() def file_diff(self, left, right, identical_msg=None): with self: identical = self.apply_diff(identical_msg or _('The files are identical'), *file_diff(left, right)) self.view.finalize() if identical: self.reject() def string_diff(self, left, right, **kw): with self: identical = self.apply_diff(kw.pop('identical_msg', None) or _('No differences found'), *string_diff(left, right, **kw)) self.view.finalize() if identical: self.reject() def dir_diff(self, left, right, identical_msg=None): with self: identical = self.apply_diff(identical_msg or _('The folders are identical'), *dir_diff(left, right)) self.view.finalize() if identical: self.reject() def apply_diff(self, identical_msg, cache, syntax_map, changed_names, renamed_names, removed_names, added_names): self.view.clear() self.apply_diff_calls = calls = [] def add(args, kwargs): self.view.add_diff(*args, **kwargs) calls.append((args, kwargs)) if len(changed_names) + len(renamed_names) + len(removed_names) + len(added_names) < 1: self.busy.setVisible(False) info_dialog(self, _('No changes found'), identical_msg, show=True) self.busy.setVisible(True) return True kwargs = lambda name: {'context':self.context, 'beautify':self.beautify, 'syntax':syntax_map.get(name, None)} if isinstance(changed_names, dict): for name, other_name in sorted(iteritems(changed_names), key=lambda x:numeric_sort_key(x[0])): args = (name, other_name, cache.left(name), cache.right(other_name)) add(args, kwargs(name)) else: for name in sorted(changed_names, key=numeric_sort_key): args = (name, name, cache.left(name), cache.right(name)) add(args, kwargs(name)) for name in sorted(added_names, key=numeric_sort_key): args = (_('[%s was added]') % name, name, None, cache.right(name)) add(args, kwargs(name)) for name in sorted(removed_names, key=numeric_sort_key): args = (name, _('[%s was removed]') % name, cache.left(name), None) add(args, kwargs(name)) for name, new_name in sorted(iteritems(renamed_names), key=lambda x:numeric_sort_key(x[0])): args = (name, new_name, None, None) add(args, kwargs(name)) def keyPressEvent(self, ev): if not self.view.handle_key(ev): if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): return # The enter key is used by the search box, so prevent it closing the dialog if ev.key() == Qt.Key.Key_Slash: return self.search.setFocus(Qt.FocusReason.OtherFocusReason) if ev.matches(QKeySequence.StandardKey.Copy): text = self.view.view.left.selected_text + self.view.view.right.selected_text if text: QApplication.clipboard().setText(text) return if ev.matches(QKeySequence.StandardKey.FindNext): self.sbn.click() return if ev.matches(QKeySequence.StandardKey.FindPrevious): self.sbp.click() return return Dialog.keyPressEvent(self, ev)
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()
class MainTab(QWidget): # {{{ changed_signal = pyqtSignal() start_server = pyqtSignal() stop_server = pyqtSignal() test_server = pyqtSignal() show_logs = pyqtSignal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = l = QVBoxLayout(self) self.la = la = QLabel( _( 'calibre contains an internet server that allows you to' ' access your book collection using a browser from anywhere' ' in the world. Any changes to the settings will only take' ' effect after a server restart.' ) ) la.setWordWrap(True) l.addWidget(la) l.addSpacing(10) self.fl = fl = QFormLayout() l.addLayout(fl) self.opt_port = sb = QSpinBox(self) if options['port'].longdoc: sb.setToolTip(options['port'].longdoc) sb.setRange(1, 65535) sb.valueChanged.connect(self.changed_signal.emit) fl.addRow(options['port'].shortdoc + ':', sb) l.addSpacing(25) self.opt_auth = cb = QCheckBox( _('Require &username and password to access the Content server') ) l.addWidget(cb) self.auth_desc = la = QLabel(self) la.setStyleSheet('QLabel { font-size: small; font-style: italic }') la.setWordWrap(True) l.addWidget(la) l.addSpacing(25) self.opt_autolaunch_server = al = QCheckBox( _('Run server &automatically when calibre starts') ) l.addWidget(al) l.addSpacing(25) self.h = h = QHBoxLayout() l.addLayout(h) for text, name in [(_('&Start server'), 'start_server'), (_('St&op server'), 'stop_server'), (_('&Test server'), 'test_server'), (_('Show server &logs'), 'show_logs')]: b = QPushButton(text) b.clicked.connect(getattr(self, name).emit) setattr(self, name + '_button', b) if name == 'show_logs': h.addStretch(10) h.addWidget(b) self.ip_info = QLabel(self) self.update_ip_info() from calibre.gui2.ui import get_gui gui = get_gui() if gui is not None: gui.iactions['Connect Share'].share_conn_menu.server_state_changed_signal.connect(self.update_ip_info) l.addSpacing(10) l.addWidget(self.ip_info) if set_run_at_startup is not None: self.run_at_start_button = b = QPushButton('', self) self.set_run_at_start_text() b.clicked.connect(self.toggle_run_at_startup) l.addSpacing(10) l.addWidget(b) l.addSpacing(10) l.addStretch(10) def set_run_at_start_text(self): is_autostarted = is_set_to_run_at_startup() self.run_at_start_button.setText( _('Do not start calibre automatically when computer is started') if is_autostarted else _('Start calibre when the computer is started') ) self.run_at_start_button.setToolTip('<p>' + ( _('''Currently calibre is set to run automatically when the computer starts. Use this button to disable that.''') if is_autostarted else _('''Start calibre in the system tray automatically when the computer starts'''))) def toggle_run_at_startup(self): set_run_at_startup(not is_set_to_run_at_startup()) self.set_run_at_start_text() def update_ip_info(self): from calibre.gui2.ui import get_gui gui = get_gui() if gui is not None: t = get_gui().iactions['Connect Share'].share_conn_menu.ip_text t = t.strip().strip('[]') self.ip_info.setText(_('Content server listening at: %s') % t) def genesis(self): opts = server_config() self.opt_auth.setChecked(opts.auth) self.opt_auth.stateChanged.connect(self.auth_changed) self.opt_port.setValue(opts.port) self.change_auth_desc() self.update_button_state() def change_auth_desc(self): self.auth_desc.setText( _('Remember to create at least one user account in the "User accounts" tab') if self.opt_auth.isChecked() else _( 'Requiring a username/password prevents unauthorized people from' ' accessing your calibre library. It is also needed for some features' ' such as making any changes to the library as well as' ' last read position/annotation syncing.' ) ) def auth_changed(self): self.changed_signal.emit() self.change_auth_desc() def restore_defaults(self): self.opt_auth.setChecked(options['auth'].default) self.opt_port.setValue(options['port'].default) def update_button_state(self): from calibre.gui2.ui import get_gui gui = get_gui() if gui is not None: is_running = gui.content_server is not None and gui.content_server.is_running self.ip_info.setVisible(is_running) self.update_ip_info() self.start_server_button.setEnabled(not is_running) self.stop_server_button.setEnabled(is_running) self.test_server_button.setEnabled(is_running) @property def settings(self): return {'auth': self.opt_auth.isChecked(), 'port': self.opt_port.value()}
class TrimImage(QDialog): def __init__(self, img_data, parent=None): QDialog.__init__(self, parent) self.l = l = QVBoxLayout(self) self.setWindowTitle(_('Trim Image')) self.bar = b = QToolBar(self) l.addWidget(b) b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) b.setIconSize(QSize(32, 32)) self.msg = la = QLabel('\xa0' + _( 'Select a region by dragging with your mouse, and then click trim') ) self.msg_txt = self.msg.text() self.sz = QLabel('') self.canvas = c = Canvas(self) c.image_changed.connect(self.image_changed) c.load_image(img_data) self.undo_action = u = c.undo_action u.setShortcut(QKeySequence(QKeySequence.StandardKey.Undo)) self.redo_action = r = c.redo_action r.setShortcut(QKeySequence(QKeySequence.StandardKey.Redo)) self.trim_action = ac = self.bar.addAction(QIcon(I('trim.png')), _('&Trim'), self.do_trim) ac.setShortcut(QKeySequence('Ctrl+T')) ac.setToolTip('{} [{}]'.format( _('Trim image by removing borders outside the selected region'), ac.shortcut().toString(QKeySequence.SequenceFormat.NativeText))) ac.setEnabled(False) c.selection_state_changed.connect(self.selection_changed) c.selection_area_changed.connect(self.selection_area_changed) l.addWidget(c) self.bar.addAction(self.trim_action) self.bar.addSeparator() self.bar.addAction(u) self.bar.addAction(r) self.bar.addSeparator() self.bar.addWidget(la) self.bar.addSeparator() self.bar.addWidget(self.sz) self.bb = bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) h = QHBoxLayout() l.addLayout(h) self.tr_sz = QLabel('') h.addWidget(self.tr_sz) h.addStretch(10) h.addWidget(bb) self.resize(QSize(900, 600)) geom = gprefs.get('image-trim-dialog-geometry', None) if geom is not None: QApplication.instance().safe_restore_geometry(self, geom) self.setWindowIcon(self.trim_action.icon()) self.image_data = None def do_trim(self): self.canvas.trim_image() self.selection_changed(False) def selection_changed(self, has_selection): self.trim_action.setEnabled(has_selection) self.msg.setText( _('Adjust selection by dragging corners' ) if has_selection else self.msg_txt) def selection_area_changed(self, rect): if rect: x, y, w, h = map(int, self.canvas.rect_for_trim()) text = f'{int(w)}x{int(h)}' text = _('Size: {0}px Aspect ratio: {1:.3g}').format(text, w / h) else: text = '' self.tr_sz.setText(text) def image_changed(self, qimage): self.sz.setText( '\xa0' + _('Size: {0}x{1}px').format(qimage.width(), qimage.height())) def cleanup(self): self.canvas.break_cycles() gprefs.set('image-trim-dialog-geometry', bytearray(self.saveGeometry())) def accept(self): if self.trim_action.isEnabled(): self.trim_action.trigger() if self.canvas.is_modified: self.image_data = self.canvas.get_image_data() self.cleanup() QDialog.accept(self) def reject(self): self.cleanup() QDialog.reject(self)
class DBRestore(QDialog): update_signal = pyqtSignal(object, object) def __init__(self, parent, library_path, wait_time=2): QDialog.__init__(self, parent) self.l = QVBoxLayout() self.setLayout(self.l) self.l1 = QLabel('<b>'+_('Restoring database from backups, do not' ' interrupt, this will happen in three stages')+'...') self.setWindowTitle(_('Restoring database')) self.l.addWidget(self.l1) self.pb = QProgressBar(self) self.l.addWidget(self.pb) self.pb.setMaximum(0) self.pb.setMinimum(0) self.msg = QLabel('') self.l.addWidget(self.msg) self.msg.setWordWrap(True) self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel) self.l.addWidget(self.bb) self.bb.rejected.connect(self.confirm_cancel) self.resize(self.sizeHint() + QSize(100, 50)) self.error = None self.rejected = False self.library_path = library_path self.update_signal.connect(self.do_update, type=Qt.ConnectionType.QueuedConnection) from calibre.db.restore import Restore self.restorer = Restore(library_path, self) self.restorer.daemon = True # Give the metadata backup thread time to stop QTimer.singleShot(wait_time * 1000, self.start) def start(self): self.restorer.start() QTimer.singleShot(10, self.update) def reject(self): self.rejected = True self.restorer.progress_callback = lambda x, y: x QDialog.reject(self) def confirm_cancel(self): if question_dialog(self, _('Are you sure?'), _( 'The restore has not completed, are you sure you want to cancel?'), default_yes=False, override_icon='dialog_warning.png'): self.reject() def update(self): if self.restorer.is_alive(): QTimer.singleShot(10, self.update) else: self.restorer.progress_callback = lambda x, y: x self.accept() def __call__(self, msg, step): self.update_signal.emit(msg, step) def do_update(self, msg, step): if msg is None: self.pb.setMaximum(step) else: self.msg.setText(msg) self.pb.setValue(step)
class JobsButton(QWidget): # {{{ tray_tooltip_updated = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) self.num_jobs = 0 self.mouse_over = False self.pi = ProgressIndicator(self, self.style().pixelMetric(QStyle.PixelMetric.PM_ToolBarIconSize)) self.pi.setVisible(False) self._jobs = QLabel('') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent self.update_label() self.shortcut = 'Alt+Shift+J' self.l = l = QHBoxLayout(self) l.setSpacing(3) l.addWidget(self.pi) l.addWidget(self._jobs) m = self.style().pixelMetric(QStyle.PixelMetric.PM_DefaultFrameWidth) self.layout().setContentsMargins(m, m, m, m) self._jobs.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) self.setCursor(Qt.CursorShape.PointingHandCursor) b = _('Click to see list of jobs') self.setToolTip(b + _(' [Alt+Shift+J]')) self.action_toggle = QAction(b, parent) parent.addAction(self.action_toggle) self.action_toggle.triggered.connect(self.toggle) if hasattr(parent, 'keyboard'): parent.keyboard.register_shortcut('toggle jobs list', _('Show/hide the Jobs List'), default_keys=(self.shortcut,), action=self.action_toggle) def update_label(self): n = self.jobs() prefix = '<b>' if n > 0 else '' self._jobs.setText(prefix + ' ' + _('Jobs:') + f' {n} ') def event(self, ev): m = None et = ev.type() if et == QEvent.Type.Enter: m = True elif et == QEvent.Type.Leave: m = False if m is not None and m != self.mouse_over: self.mouse_over = m self.update() return QWidget.event(self, ev) def initialize(self, jobs_dialog, job_manager): self.jobs_dialog = jobs_dialog job_manager.job_added.connect(self.job_added) job_manager.job_done.connect(self.job_done) self.jobs_dialog.addAction(self.action_toggle) def mouseReleaseEvent(self, event): self.toggle() def toggle(self, *args): if self.jobs_dialog.isVisible(): self.jobs_dialog.hide() else: self.jobs_dialog.show() @property def is_running(self): return self.pi.isAnimated() def start(self): self.pi.startAnimation() self.pi.setVisible(True) def stop(self): self.pi.stopAnimation() self.pi.setVisible(False) def jobs(self): return self.num_jobs def tray_tooltip(self, num=0): if num == 0: text = _('No running jobs') elif num == 1: text = _('One running job') else: text = _('%d running jobs') % num if not (islinux or isbsd): text = 'calibre: ' + text return text def job_added(self, nnum): self.num_jobs = nnum self.update_label() self.start() self.tray_tooltip_updated.emit(self.tray_tooltip(nnum)) def job_done(self, nnum): self.num_jobs = nnum self.update_label() if nnum == 0: self.no_more_jobs() self.tray_tooltip_updated.emit(self.tray_tooltip(nnum)) def no_more_jobs(self): if self.is_running: self.stop() QCoreApplication.instance().alert(self, 5000) def paintEvent(self, ev): if self.mouse_over: p = QStylePainter(self) tool = QStyleOption() tool.initFrom(self) tool.rect = self.rect() tool.state = QStyle.StateFlag.State_Raised | QStyle.StateFlag.State_Active | QStyle.StateFlag.State_MouseOver p.drawPrimitive(QStyle.PrimitiveElement.PE_PanelButtonTool, tool) p.end() QWidget.paintEvent(self, ev)
class PluginUpdaterDialog(SizePersistedDialog): initial_extra_size = QSize(350, 100) forum_label_text = _('Plugin homepage') def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE): SizePersistedDialog.__init__( self, gui, 'Plugin Updater plugin:plugin updater dialog') self.gui = gui self.forum_link = None self.zip_url = None self.model = None self.do_restart = False self._initialize_controls() self._create_context_menu() try: display_plugins = read_available_plugins(raise_error=True) except Exception: display_plugins = [] import traceback error_dialog(self.gui, _('Update Check Failed'), _('Unable to reach the plugin index page.'), det_msg=traceback.format_exc(), show=True) if display_plugins: self.model = DisplayPluginModel(display_plugins) self.proxy_model = DisplayPluginSortFilterModel(self) self.proxy_model.setSourceModel(self.model) self.plugin_view.setModel(self.proxy_model) self.plugin_view.resizeColumnsToContents() self.plugin_view.selectionModel().currentRowChanged.connect( self._plugin_current_changed) self.plugin_view.doubleClicked.connect(self.install_button.click) self.filter_combo.setCurrentIndex(initial_filter) self._select_and_focus_view() else: self.filter_combo.setEnabled(False) # Cause our dialog size to be restored from prefs or created on first usage self.resize_dialog() def _initialize_controls(self): self.setWindowTitle(_('User plugins')) self.setWindowIcon(QIcon(I('plugins/plugin_updater.png'))) layout = QVBoxLayout(self) self.setLayout(layout) title_layout = ImageTitleLayout(self, 'plugins/plugin_updater.png', _('User plugins')) layout.addLayout(title_layout) header_layout = QHBoxLayout() layout.addLayout(header_layout) self.filter_combo = PluginFilterComboBox(self) self.filter_combo.setMinimumContentsLength(20) self.filter_combo.currentIndexChanged[int].connect( self._filter_combo_changed) la = QLabel(_('Filter list of &plugins') + ':', self) la.setBuddy(self.filter_combo) header_layout.addWidget(la) header_layout.addWidget(self.filter_combo) header_layout.addStretch(10) # filter plugins by name la = QLabel(_('Filter by &name') + ':', self) header_layout.addWidget(la) self.filter_by_name_lineedit = QLineEdit(self) la.setBuddy(self.filter_by_name_lineedit) self.filter_by_name_lineedit.setText("") self.filter_by_name_lineedit.textChanged.connect( self._filter_name_lineedit_changed) header_layout.addWidget(self.filter_by_name_lineedit) self.plugin_view = QTableView(self) self.plugin_view.horizontalHeader().setStretchLastSection(True) self.plugin_view.setSelectionBehavior( QAbstractItemView.SelectionBehavior.SelectRows) self.plugin_view.setSelectionMode( QAbstractItemView.SelectionMode.SingleSelection) self.plugin_view.setAlternatingRowColors(True) self.plugin_view.setSortingEnabled(True) self.plugin_view.setIconSize(QSize(28, 28)) layout.addWidget(self.plugin_view) details_layout = QHBoxLayout() layout.addLayout(details_layout) forum_label = self.forum_label = QLabel('') forum_label.setTextInteractionFlags( Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard) forum_label.linkActivated.connect(self._forum_label_activated) details_layout.addWidget(QLabel(_('Description') + ':', self), 0, Qt.AlignmentFlag.AlignLeft) details_layout.addWidget(forum_label, 1, Qt.AlignmentFlag.AlignRight) self.description = QLabel(self) self.description.setFrameStyle(QFrame.Shape.Panel | QFrame.Shadow.Sunken) self.description.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) self.description.setMinimumHeight(40) self.description.setWordWrap(True) layout.addWidget(self.description) self.button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Close) self.button_box.rejected.connect(self.reject) self.finished.connect(self._finished) self.install_button = self.button_box.addButton( _('&Install'), QDialogButtonBox.ButtonRole.AcceptRole) self.install_button.setToolTip(_('Install the selected plugin')) self.install_button.clicked.connect(self._install_clicked) self.install_button.setEnabled(False) self.configure_button = self.button_box.addButton( ' ' + _('&Customize plugin ') + ' ', QDialogButtonBox.ButtonRole.ResetRole) self.configure_button.setToolTip( _('Customize the options for this plugin')) self.configure_button.clicked.connect(self._configure_clicked) self.configure_button.setEnabled(False) layout.addWidget(self.button_box) def update_forum_label(self): txt = '' if self.forum_link: txt = '<a href="%s">%s</a>' % (self.forum_link, self.forum_label_text) self.forum_label.setText(txt) def _create_context_menu(self): self.plugin_view.setContextMenuPolicy( Qt.ContextMenuPolicy.ActionsContextMenu) self.install_action = QAction( QIcon(I('plugins/plugin_upgrade_ok.png')), _('&Install'), self) self.install_action.setToolTip(_('Install the selected plugin')) self.install_action.triggered.connect(self._install_clicked) self.install_action.setEnabled(False) self.plugin_view.addAction(self.install_action) self.forum_action = QAction(QIcon(I('plugins/mobileread.png')), _('Plugin &forum thread'), self) self.forum_action.triggered.connect(self._forum_label_activated) self.forum_action.setEnabled(False) self.plugin_view.addAction(self.forum_action) sep1 = QAction(self) sep1.setSeparator(True) self.plugin_view.addAction(sep1) self.toggle_enabled_action = QAction(_('Enable/&disable plugin'), self) self.toggle_enabled_action.setToolTip( _('Enable or disable this plugin')) self.toggle_enabled_action.triggered.connect( self._toggle_enabled_clicked) self.toggle_enabled_action.setEnabled(False) self.plugin_view.addAction(self.toggle_enabled_action) self.uninstall_action = QAction(_('&Remove plugin'), self) self.uninstall_action.setToolTip(_('Uninstall the selected plugin')) self.uninstall_action.triggered.connect(self._uninstall_clicked) self.uninstall_action.setEnabled(False) self.plugin_view.addAction(self.uninstall_action) sep2 = QAction(self) sep2.setSeparator(True) self.plugin_view.addAction(sep2) self.donate_enabled_action = QAction(QIcon(I('donate.png')), _('Donate to developer'), self) self.donate_enabled_action.setToolTip( _('Donate to the developer of this plugin')) self.donate_enabled_action.triggered.connect(self._donate_clicked) self.donate_enabled_action.setEnabled(False) self.plugin_view.addAction(self.donate_enabled_action) sep3 = QAction(self) sep3.setSeparator(True) self.plugin_view.addAction(sep3) self.configure_action = QAction(QIcon(I('config.png')), _('&Customize plugin'), self) self.configure_action.setToolTip( _('Customize the options for this plugin')) self.configure_action.triggered.connect(self._configure_clicked) self.configure_action.setEnabled(False) self.plugin_view.addAction(self.configure_action) def _finished(self, *args): if self.model: update_plugins = list( filter(filter_upgradeable_plugins, self.model.display_plugins)) self.gui.recalc_update_label(len(update_plugins)) def _plugin_current_changed(self, current, previous): if current.isValid(): actual_idx = self.proxy_model.mapToSource(current) display_plugin = self.model.display_plugins[actual_idx.row()] self.description.setText(display_plugin.description) self.forum_link = display_plugin.forum_link self.zip_url = display_plugin.zip_url self.forum_action.setEnabled(bool(self.forum_link)) self.install_button.setEnabled( display_plugin.is_valid_to_install()) self.install_action.setEnabled(self.install_button.isEnabled()) self.uninstall_action.setEnabled(display_plugin.is_installed()) self.configure_button.setEnabled(display_plugin.is_installed()) self.configure_action.setEnabled(self.configure_button.isEnabled()) self.toggle_enabled_action.setEnabled( display_plugin.is_installed()) self.donate_enabled_action.setEnabled( bool(display_plugin.donation_link)) else: self.description.setText('') self.forum_link = None self.zip_url = None self.forum_action.setEnabled(False) self.install_button.setEnabled(False) self.install_action.setEnabled(False) self.uninstall_action.setEnabled(False) self.configure_button.setEnabled(False) self.configure_action.setEnabled(False) self.toggle_enabled_action.setEnabled(False) self.donate_enabled_action.setEnabled(False) self.update_forum_label() def _donate_clicked(self): plugin = self._selected_display_plugin() if plugin and plugin.donation_link: open_url(QUrl(plugin.donation_link)) def _select_and_focus_view(self, change_selection=True): if change_selection and self.plugin_view.model().rowCount() > 0: self.plugin_view.selectRow(0) else: idx = self.plugin_view.selectionModel().currentIndex() self._plugin_current_changed(idx, 0) self.plugin_view.setFocus() def _filter_combo_changed(self, idx): self.filter_by_name_lineedit.setText( "" ) # clear the name filter text when a different group was selected self.proxy_model.set_filter_criteria(idx) if idx == FILTER_NOT_INSTALLED: self.plugin_view.sortByColumn(5, Qt.SortOrder.DescendingOrder) else: self.plugin_view.sortByColumn(0, Qt.SortOrder.AscendingOrder) self._select_and_focus_view() def _filter_name_lineedit_changed(self, text): self.proxy_model.set_filter_text( text) # set the filter text for filterAcceptsRow def _forum_label_activated(self): if self.forum_link: open_url(QUrl(self.forum_link)) def _selected_display_plugin(self): idx = self.plugin_view.selectionModel().currentIndex() actual_idx = self.proxy_model.mapToSource(idx) return self.model.display_plugins[actual_idx.row()] def _uninstall_plugin(self, name_to_remove): if DEBUG: prints('Removing plugin: ', name_to_remove) remove_plugin(name_to_remove) # Make sure that any other plugins that required this plugin # to be uninstalled first have the requirement removed for display_plugin in self.model.display_plugins: # Make sure we update the status and display of the # plugin we just uninstalled if name_to_remove in display_plugin.uninstall_plugins: if DEBUG: prints('Removing uninstall dependency for: ', display_plugin.name) display_plugin.uninstall_plugins.remove(name_to_remove) if display_plugin.qname == name_to_remove: if DEBUG: prints('Resetting plugin to uninstalled status: ', display_plugin.name) display_plugin.installed_version = None display_plugin.plugin = None display_plugin.uninstall_plugins = [] if self.proxy_model.filter_criteria not in [ FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE ]: self.model.refresh_plugin(display_plugin) def _uninstall_clicked(self): display_plugin = self._selected_display_plugin() if not question_dialog( self, _('Are you sure?'), '<p>' + _('Are you sure you want to uninstall the <b>%s</b> plugin?') % display_plugin.name, show_copy_button=False): return self._uninstall_plugin(display_plugin.qname) if self.proxy_model.filter_criteria in [ FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE ]: self.model.beginResetModel(), self.model.endResetModel() self._select_and_focus_view() else: self._select_and_focus_view(change_selection=False) def _install_clicked(self): display_plugin = self._selected_display_plugin() if not question_dialog( self, _('Install %s') % display_plugin.name, '<p>' + _('Installing plugins is a <b>security risk</b>. ' 'Plugins can contain a virus/malware. ' 'Only install it if you got it from a trusted source.' ' Are you sure you want to proceed?'), show_copy_button=False): return if display_plugin.uninstall_plugins: uninstall_names = list(display_plugin.uninstall_plugins) if DEBUG: prints('Uninstalling plugin: ', ', '.join(uninstall_names)) for name_to_remove in uninstall_names: self._uninstall_plugin(name_to_remove) plugin_zip_url = display_plugin.zip_url if DEBUG: prints('Downloading plugin ZIP attachment: ', plugin_zip_url) self.gui.status_bar.showMessage( _('Downloading plugin ZIP attachment: %s') % plugin_zip_url) zip_path = self._download_zip(plugin_zip_url) if DEBUG: prints('Installing plugin: ', zip_path) self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path) do_restart = False try: from calibre.customize.ui import config installed_plugins = frozenset(config['plugins']) try: plugin = add_plugin(zip_path) except NameConflict as e: return error_dialog(self.gui, _('Already exists'), unicode_type(e), show=True) # Check for any toolbars to add to. widget = ConfigWidget(self.gui) widget.gui = self.gui widget.check_for_add_to_toolbars(plugin, previously_installed=plugin.name in installed_plugins) self.gui.status_bar.showMessage( _('Plugin installed: %s') % display_plugin.name) d = info_dialog( self.gui, _('Success'), _('Plugin <b>{0}</b> successfully installed under <b>' '{1}</b>. You may have to restart calibre ' 'for the plugin to take effect.').format( plugin.name, plugin.type), show_copy_button=False) b = d.bb.addButton(_('&Restart calibre now'), QDialogButtonBox.ButtonRole.AcceptRole) b.setIcon(QIcon(I('lt.png'))) d.do_restart = False def rf(): d.do_restart = True b.clicked.connect(rf) d.set_details('') d.exec_() b.clicked.disconnect() do_restart = d.do_restart display_plugin.plugin = plugin # We cannot read the 'actual' version information as the plugin will not be loaded yet display_plugin.installed_version = display_plugin.available_version except: if DEBUG: prints('ERROR occurred while installing plugin: %s' % display_plugin.name) traceback.print_exc() error_dialog( self.gui, _('Install plugin failed'), _('A problem occurred while installing this plugin.' ' This plugin will now be uninstalled.' ' Please post the error message in details below into' ' the forum thread for this plugin and restart calibre.'), det_msg=traceback.format_exc(), show=True) if DEBUG: prints('Due to error now uninstalling plugin: %s' % display_plugin.name) remove_plugin(display_plugin.name) display_plugin.plugin = None display_plugin.uninstall_plugins = [] if self.proxy_model.filter_criteria in [ FILTER_NOT_INSTALLED, FILTER_UPDATE_AVAILABLE ]: self.model.beginResetModel(), self.model.endResetModel() self._select_and_focus_view() else: self.model.refresh_plugin(display_plugin) self._select_and_focus_view(change_selection=False) if do_restart: self.do_restart = True self.accept() def _configure_clicked(self): display_plugin = self._selected_display_plugin() plugin = display_plugin.plugin if not plugin.is_customizable(): return info_dialog(self, _('Plugin not customizable'), _('Plugin: %s does not need customization') % plugin.name, show=True) from calibre.customize import InterfaceActionBase if isinstance(plugin, InterfaceActionBase) and not getattr( plugin, 'actual_iaction_plugin_loaded', False): return error_dialog(self, _('Must restart'), _('You must restart calibre before you can' ' configure the <b>%s</b> plugin') % plugin.name, show=True) plugin.do_user_config(self.parent()) def _toggle_enabled_clicked(self): display_plugin = self._selected_display_plugin() plugin = display_plugin.plugin if not plugin.can_be_disabled: return error_dialog(self, _('Plugin cannot be disabled'), _('The plugin: %s cannot be disabled') % plugin.name, show=True) if is_disabled(plugin): enable_plugin(plugin) else: disable_plugin(plugin) self.model.refresh_plugin(display_plugin) def _download_zip(self, plugin_zip_url): from calibre.ptempfile import PersistentTemporaryFile raw = get_https_resource_securely( plugin_zip_url, headers={'User-Agent': '%s %s' % (__appname__, __version__)}) with PersistentTemporaryFile('.zip') as pt: pt.write(raw) return pt.name
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)
class IdentifyWidget(QWidget): # {{{ rejected = pyqtSignal() results_found = pyqtSignal() book_selected = pyqtSignal(object, object) def __init__(self, log, parent=None): QWidget.__init__(self, parent) self.log = log self.abort = Event() self.caches = {} self.l = l = QVBoxLayout(self) names = [ '<b>' + p.name + '</b>' for p in metadata_plugins(['identify']) if p.is_configured() ] self.top = QLabel('<p>' + _('calibre is downloading metadata from: ') + ', '.join(names)) self.top.setWordWrap(True) l.addWidget(self.top) self.splitter = s = QSplitter(self) s.setChildrenCollapsible(False) l.addWidget(s, 100) self.results_view = ResultsView(self) self.results_view.book_selected.connect(self.emit_book_selected) self.get_result = self.results_view.get_result s.addWidget(self.results_view) self.comments_view = Comments(self) s.addWidget(self.comments_view) s.setStretchFactor(0, 2) s.setStretchFactor(1, 1) self.results_view.show_details_signal.connect( self.comments_view.show_data) self.query = QLabel('download starting...') self.query.setWordWrap(True) self.query.setTextFormat(Qt.TextFormat.PlainText) l.addWidget(self.query) self.comments_view.show_wait() state = gprefs.get('metadata-download-identify-widget-splitter-state') if state is not None: s.restoreState(state) def save_state(self): gprefs['metadata-download-identify-widget-splitter-state'] = bytearray( self.splitter.saveState()) def emit_book_selected(self, book): self.book_selected.emit(book, self.caches) def start(self, title=None, authors=None, identifiers={}): self.log.clear() self.log('Starting download') parts, simple_desc = [], '' if title: parts.append('title:' + title) simple_desc += _('Title: %s ') % title if authors: parts.append('authors:' + authors_to_string(authors)) simple_desc += _('Authors: %s ') % authors_to_string(authors) if identifiers: x = ', '.join('%s:%s' % (k, v) for k, v in iteritems(identifiers)) parts.append(x) if 'isbn' in identifiers: simple_desc += 'ISBN: %s' % identifiers['isbn'] self.query.setText(simple_desc) self.log(str(self.query.text())) self.worker = IdentifyWorker(self.log, self.abort, title, authors, identifiers, self.caches) self.worker.start() QTimer.singleShot(50, self.update) def update(self): if self.worker.is_alive(): QTimer.singleShot(50, self.update) else: self.process_results() def process_results(self): if self.worker.error is not None: error_dialog(self, _('Download failed'), _('Failed to download metadata. Click ' 'Show Details to see details'), show=True, det_msg=self.worker.error) self.rejected.emit() return if not self.worker.results: log = ''.join(self.log.plain_text) error_dialog( self, _('No matches found'), '<p>' + _('Failed to find any books that ' 'match your search. Try making the search <b>less ' 'specific</b>. For example, use only the author\'s ' 'last name and a single distinctive word from ' 'the title.<p>To see the full log, click "Show details".'), show=True, det_msg=log) self.rejected.emit() return self.results_view.show_results(self.worker.results) self.results_found.emit() def cancel(self): self.abort.set()
class XPathEdit(QWidget): def __init__(self, parent=None, object_name='', show_msg=True): QWidget.__init__(self, parent) self.h = h = QHBoxLayout(self) h.setContentsMargins(0, 0, 0, 0) self.l = l = QVBoxLayout() h.addLayout(l) self.button = b = QToolButton(self) b.setIcon(QIcon(I('wizard.png'))) b.setToolTip(_('Use a wizard to generate the XPath expression')) b.clicked.connect(self.wizard) h.addWidget(b) self.edit = e = HistoryLineEdit(self) e.setMinimumWidth(350) e.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) e.setMinimumContentsLength(30) self.msg = QLabel('') l.addWidget(self.msg) l.addWidget(self.edit) if object_name: self.setObjectName(object_name) if show_msg: b.setIconSize(QSize(40, 40)) self.msg.setBuddy(self.edit) else: self.msg.setVisible(False) l.setContentsMargins(0, 0, 0, 0) def setPlaceholderText(self, val): self.edit.setPlaceholderText(val) def wizard(self): wiz = Wizard(self) if wiz.exec() == QDialog.DialogCode.Accepted: self.edit.setText(wiz.xpath) def setObjectName(self, *args): QWidget.setObjectName(self, *args) if hasattr(self, 'edit'): self.edit.initialize('xpath_edit_'+str(self.objectName())) def set_msg(self, msg): self.msg.setText(msg) @property def text(self): return str(self.edit.text()) @text.setter def text(self, val): self.edit.setText(str(val)) value = text @property def xpath(self): return self.text def check(self): from calibre.ebooks.oeb.base import XPath try: if self.text.strip(): XPath(self.text) except: import traceback traceback.print_exc() return False return True
class Editor(QFrame): # {{{ editing_done = pyqtSignal(object) def __init__(self, parent=None): QFrame.__init__(self, parent) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.setAutoFillBackground(True) self.capture = 0 self.setFrameShape(QFrame.Shape.StyledPanel) self.setFrameShadow(QFrame.Shadow.Raised) self._layout = l = QGridLayout(self) self.setLayout(l) self.header = QLabel('') l.addWidget(self.header, 0, 0, 1, 2) self.use_default = QRadioButton('') self.use_custom = QRadioButton(_('&Custom')) l.addWidget(self.use_default, 1, 0, 1, 3) l.addWidget(self.use_custom, 2, 0, 1, 3) self.use_custom.toggled.connect(self.custom_toggled) off = 2 for which in (1, 2): text = _('&Shortcut:') if which == 1 else _('&Alternate shortcut:') la = QLabel(text) la.setStyleSheet('QLabel { margin-left: 1.5em }') l.addWidget(la, off + which, 0, 1, 3) setattr(self, 'label%d' % which, la) button = QPushButton(_('None'), self) button.clicked.connect(partial(self.capture_clicked, which=which)) button.installEventFilter(self) setattr(self, 'button%d' % which, button) clear = QToolButton(self) clear.setIcon(QIcon(I('clear_left.png'))) clear.clicked.connect(partial(self.clear_clicked, which=which)) setattr(self, 'clear%d' % which, clear) l.addWidget(button, off + which, 1, 1, 1) l.addWidget(clear, off + which, 2, 1, 1) la.setBuddy(button) self.done_button = doneb = QPushButton(_('Done'), self) l.addWidget(doneb, 0, 2, 1, 1) doneb.clicked.connect(lambda: self.editing_done.emit(self)) l.setColumnStretch(0, 100) self.custom_toggled(False) def initialize(self, shortcut, all_shortcuts): self.header.setText('<b>%s: %s</b>' % (_('Customize'), shortcut['name'])) self.all_shortcuts = all_shortcuts self.shortcut = shortcut self.default_keys = [ QKeySequence(k, QKeySequence.SequenceFormat.PortableText) for k in shortcut['default_keys'] ] self.current_keys = list(shortcut['keys']) default = ', '.join([ unicode_type(k.toString(QKeySequence.SequenceFormat.NativeText)) for k in self.default_keys ]) if not default: default = _('None') current = ', '.join([ unicode_type(k.toString(QKeySequence.SequenceFormat.NativeText)) for k in self.current_keys ]) if not current: current = _('None') self.use_default.setText( _('&Default: %(deflt)s [Currently not conflicting: %(curr)s]') % dict(deflt=default, curr=current)) if shortcut['set_to_default']: self.use_default.setChecked(True) else: self.use_custom.setChecked(True) for key, which in zip(self.current_keys, [1, 2]): button = getattr(self, 'button%d' % which) button.setText( key.toString(QKeySequence.SequenceFormat.NativeText)) def custom_toggled(self, checked): for w in ('1', '2'): for o in ('label', 'button', 'clear'): getattr(self, o + w).setEnabled(checked) def capture_clicked(self, which=1): self.capture = which button = getattr(self, 'button%d' % which) button.setText(_('Press a key...')) button.setFocus(Qt.FocusReason.OtherFocusReason) button.setStyleSheet('QPushButton { font-weight: bold}') def clear_clicked(self, which=0): button = getattr(self, 'button%d' % which) button.setText(_('None')) def eventFilter(self, obj, event): if self.capture and obj in (self.button1, self.button2): t = event.type() if t == QEvent.Type.ShortcutOverride: event.accept() return True if t == QEvent.Type.KeyPress: self.key_press_event(event, 1 if obj is self.button1 else 2) return True return QFrame.eventFilter(self, obj, event) def key_press_event(self, ev, which=0): if self.capture == 0: return QWidget.keyPressEvent(self, ev) sequence = keysequence_from_event(ev) if sequence is None: return QWidget.keyPressEvent(self, ev) ev.accept() button = getattr(self, 'button%d' % which) button.setStyleSheet('QPushButton { font-weight: normal}') button.setText( sequence.toString(QKeySequence.SequenceFormat.NativeText)) self.capture = 0 dup_desc = self.dup_check(sequence) if dup_desc is not None: error_dialog(self, _('Already assigned'), unicode_type( sequence.toString( QKeySequence.SequenceFormat.NativeText)) + ' ' + _('already assigned to') + ' ' + dup_desc, show=True) self.clear_clicked(which=which) def dup_check(self, sequence): for sc in self.all_shortcuts: if sc is self.shortcut: continue for k in sc['keys']: if k == sequence: return sc['name'] @property def custom_keys(self): if self.use_default.isChecked(): return None ans = [] for which in (1, 2): button = getattr(self, 'button%d' % which) t = unicode_type(button.text()) if t == _('None'): continue ks = QKeySequence(t, QKeySequence.SequenceFormat.NativeText) if not ks.isEmpty(): ans.append(ks) return tuple(ans)
class TrimImage(QDialog): def __init__(self, img_data, parent=None): QDialog.__init__(self, parent) self.l = l = QGridLayout(self) self.setLayout(l) self.setWindowTitle(_('Trim Image')) self.bar = b = QToolBar(self) l.addWidget(b) b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) b.setIconSize(QSize(32, 32)) self.msg = la = QLabel('\xa0' + _( 'Select a region by dragging with your mouse on the image, and then click trim' )) self.sz = QLabel('') self.canvas = c = Canvas(self) c.image_changed.connect(self.image_changed) c.load_image(img_data) self.undo_action = u = c.undo_action u.setShortcut(QKeySequence(QKeySequence.StandardKey.Undo)) self.redo_action = r = c.redo_action r.setShortcut(QKeySequence(QKeySequence.StandardKey.Redo)) self.trim_action = ac = self.bar.addAction(QIcon(I('trim.png')), _('&Trim'), self.do_trim) ac.setShortcut(QKeySequence('Ctrl+T')) ac.setToolTip( '%s [%s]' % (_('Trim image by removing borders outside the selected region'), ac.shortcut().toString(QKeySequence.SequenceFormat.NativeText))) ac.setEnabled(False) c.selection_state_changed.connect(self.selection_changed) l.addWidget(c) self.bar.addAction(self.trim_action) self.bar.addSeparator() self.bar.addAction(u) self.bar.addAction(r) self.bar.addSeparator() self.bar.addWidget(la) self.bar.addSeparator() self.bar.addWidget(self.sz) self.bb = bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) l.addWidget(bb) self.resize(QSize(900, 600)) geom = gprefs.get('image-trim-dialog-geometry', None) if geom is not None: QApplication.instance().safe_restore_geometry(self, geom) self.setWindowIcon(self.trim_action.icon()) self.image_data = None def do_trim(self): self.canvas.trim_image() self.selection_changed(False) def selection_changed(self, has_selection): self.trim_action.setEnabled(has_selection) self.msg.setVisible(not has_selection) def image_changed(self, qimage): self.sz.setText('\xa0' + _('Size:') + ' ' + '%dx%d' % (qimage.width(), qimage.height())) def cleanup(self): self.canvas.break_cycles() gprefs.set('image-trim-dialog-geometry', bytearray(self.saveGeometry())) def accept(self): if self.trim_action.isEnabled(): self.trim_action.trigger() if self.canvas.is_modified: self.image_data = self.canvas.get_image_data() self.cleanup() QDialog.accept(self) def reject(self): self.cleanup() QDialog.reject(self)