class BlockingBusy(QDialog): def __init__(self, msg, parent=None, window_title=_('Working')): QDialog.__init__(self, parent) self._layout = QVBoxLayout() self.setLayout(self._layout) self.msg = QLabel(msg) # self.msg.setWordWrap(True) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) self.pi = ProgressIndicator(self) self.pi.setDisplaySize(100) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.start() self.setWindowTitle(window_title) self.resize(self.sizeHint()) def start(self): self.pi.startAnimation() def stop(self): self.pi.stopAnimation() def accept(self): self.stop() return QDialog.accept(self) def reject(self): pass # Cannot cancel this dialog
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.setVisible(True) self.pi.startAnimation() self.label.setText(_('Saving...')) def stop(self): self.pi.setVisible(False) self.pi.stopAnimation() self.label.setText(_('Saved'))
class MyBlockingBusy(QDialog): NORMAL = 0 REQUESTED = 1 ACKNOWLEDGED = 2 def __init__(self, gui, msg, size=100, window_title='Marvin XD', show_cancel=False, on_top=False): flags = Qt.FramelessWindowHint if on_top: flags = Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint QDialog.__init__(self, gui, flags) self._layout = QVBoxLayout() self.setLayout(self._layout) self.cancel_status = 0 self.is_running = False # Add the spinner self.pi = ProgressIndicator(self) self.pi.setDisplaySize(size) self._layout.addSpacing(15) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) # Fiddle with the message self.msg = QLabel(msg) #self.msg.setWordWrap(True) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 2) self.msg.setFont(self.font) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) sp = QSizePolicy() sp.setHorizontalStretch(True) sp.setVerticalStretch(False) sp.setHeightForWidth(False) self.msg.setSizePolicy(sp) self.msg.setMinimumHeight(self.font.pointSize() + 8) self._layout.addSpacing(15) if show_cancel: self.bb = QDialogButtonBox() self.cancel_button = QPushButton(QIcon(I('window-close.png')), 'Cancel') self.bb.addButton(self.cancel_button, self.bb.RejectRole) self.bb.clicked.connect(self.button_handler) self._layout.addWidget(self.bb) self.setWindowTitle(window_title) self.resize(self.sizeHint()) def accept(self): self.stop() return QDialog.accept(self) def button_handler(self, button): ''' Only change cancel_status from NORMAL to REQUESTED ''' if self.bb.buttonRole(button) == QDialogButtonBox.RejectRole: if self.cancel_status == self.NORMAL: self.cancel_status = self.REQUESTED self.cancel_button.setEnabled(False) def reject(self): ''' Cannot cancel this dialog manually ''' pass def set_text(self, text): self.msg.setText(text) def start(self): self.is_running = True self.pi.startAnimation() def stop(self): self.is_running = False self.pi.stopAnimation()
class MyBlockingBusy(QDialog): # {{{ all_done = pyqtSignal() def __init__(self, args, ids, db, refresh_books, cc_widgets, s_r_func, do_sr, sr_calls, parent=None, window_title=_('Working')): QDialog.__init__(self, parent) self._layout = l = QVBoxLayout() self.setLayout(l) self.msg = QLabel(_('Processing %d books, please wait...') % len(ids)) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) self.pi = ProgressIndicator(self) self.pi.setDisplaySize(100) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.setWindowTitle(window_title + '...') self.setMinimumWidth(200) self.resize(self.sizeHint()) self.error = None self.all_done.connect(self.on_all_done, type=Qt.QueuedConnection) self.args, self.ids = args, ids self.db, self.cc_widgets = db, cc_widgets self.s_r_func = FunctionDispatcher(s_r_func) self.do_sr = do_sr self.sr_calls = sr_calls self.refresh_books = refresh_books def accept(self): pass def reject(self): pass def on_all_done(self): if not self.error: # The cc widgets can only be accessed in the GUI thread try: for w in self.cc_widgets: w.commit(self.ids) except Exception as err: import traceback self.error = (err, traceback.format_exc()) self.pi.stopAnimation() QDialog.accept(self) def exec_(self): self.thread = Thread(target=self.do_it) self.thread.start() self.pi.startAnimation() return QDialog.exec_(self) def do_it(self): try: self.do_all() except Exception as err: import traceback try: err = unicode(err) except: err = repr(err) self.error = (err, traceback.format_exc()) self.all_done.emit() def do_all(self): cache = self.db.new_api args = self.args # Title and authors if args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) authors_map = cache.all_field_for('authors', self.ids) def new_title(authors): ans = authors_to_string(authors) return titlecase(ans) if args.do_title_case else ans new_title_map = {bid:new_title(authors) for bid, authors in authors_map.iteritems()} new_authors_map = {bid:string_to_authors(title) for bid, title in title_map.iteritems()} cache.set_field('authors', new_authors_map) cache.set_field('title', new_title_map) if args.do_title_case and not args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) cache.set_field('title', {bid:titlecase(title) for bid, title in title_map.iteritems()}) if args.do_title_sort: lang_map = cache.all_field_for('languages', self.ids) title_map = cache.all_field_for('title', self.ids) def get_sort(book_id): if args.languages: lang = args.languages[0] else: try: lang = lang_map[book_id][0] except (KeyError, IndexError, TypeError, AttributeError): lang = 'eng' return title_sort(title_map[book_id], lang=lang) cache.set_field('sort', {bid:get_sort(bid) for bid in self.ids}) if args.au: authors = string_to_authors(args.au) cache.set_field('authors', {bid:authors for bid in self.ids}) if args.do_auto_author: aus_map = cache.author_sort_strings_for_books(self.ids) cache.set_field('author_sort', {book_id:' & '.join(aus_map[book_id]) for book_id in aus_map}) if args.aus and args.do_aus: cache.set_field('author_sort', {bid:args.aus for bid in self.ids}) # Covers if args.cover_action == 'remove': cache.set_cover({bid:None for bid in self.ids}) elif args.cover_action == 'generate': from calibre.ebooks import calibre_cover from calibre.ebooks.metadata import fmt_sidx from calibre.gui2 import config for book_id in self.ids: mi = self.db.get_metadata(book_id, index_is_id=True) series_string = None if mi.series: series_string = _('Book %(sidx)s of %(series)s')%dict( sidx=fmt_sidx(mi.series_index, use_roman=config['use_roman_numerals_for_series_number']), series=mi.series) cdata = calibre_cover(mi.title, mi.format_field('authors')[-1], series_string=series_string) cache.set_cover({book_id:cdata}) elif args.cover_action == 'fromfmt': for book_id in self.ids: fmts = cache.formats(book_id, verify_formats=False) if fmts: covers = [] for fmt in fmts: fmtf = cache.format(book_id, fmt, as_file=True) if fmtf is None: continue cdata, area = get_cover_data(fmtf, fmt) if cdata: covers.append((cdata, area)) covers.sort(key=lambda x: x[1]) if covers: cache.set_cover({book_id:covers[-1][0]}) elif args.cover_action == 'trim': from calibre.utils.magick import Image for book_id in self.ids: cdata = cache.cover(book_id) if cdata: im = Image() im.load(cdata) im.trim(tweaks['cover_trim_fuzz_value']) cdata = im.export('jpg') cache.set_cover({book_id:cdata}) elif args.cover_action == 'clone': cdata = None for book_id in self.ids: cdata = cache.cover(book_id) if cdata: break if cdata: cache.set_cover({bid:cdata for bid in self.ids if bid != book_id}) # Formats if args.do_remove_format: cache.remove_formats({bid:(args.remove_format,) for bid in self.ids}) if args.restore_original: for book_id in self.ids: formats = cache.formats(book_id) originals = tuple(x.upper() for x in formats if x.upper().startswith('ORIGINAL_')) for ofmt in originals: cache.restore_original_format(book_id, ofmt) # Various fields if args.rating != -1: cache.set_field('rating', {bid:args.rating*2 for bid in self.ids}) if args.clear_pub: cache.set_field('publisher', {bid:'' for bid in self.ids}) if args.pub: cache.set_field('publisher', {bid:args.pub for bid in self.ids}) if args.clear_series: cache.set_field('series', {bid:'' for bid in self.ids}) if args.pubdate is not None: cache.set_field('pubdate', {bid:args.pubdate for bid in self.ids}) if args.adddate is not None: cache.set_field('timestamp', {bid:args.adddate for bid in self.ids}) if args.do_series: sval = args.series_start_value if args.do_series_restart else cache.get_next_series_num_for(args.series, current_indices=True) cache.set_field('series', {bid:args.series for bid in self.ids}) if not args.series: cache.set_field('series_index', {bid:1.0 for bid in self.ids}) else: def next_series_num(bid, i): if args.do_series_restart: return sval + i next_num = _get_next_series_num_for_list(sorted(sval.itervalues()), unwrap=False) sval[bid] = next_num return next_num smap = {bid:next_series_num(bid, i) for i, bid in enumerate(self.ids)} if args.do_autonumber: cache.set_field('series_index', smap) elif tweaks['series_index_auto_increment'] != 'no_change': cache.set_field('series_index', {bid:1.0 for bid in self.ids}) if args.comments is not null: cache.set_field('comments', {bid:args.comments for bid in self.ids}) if args.do_remove_conv: cache.delete_conversion_options(self.ids) if args.clear_languages: cache.set_field('languages', {bid:() for bid in self.ids}) elif args.languages: cache.set_field('languages', {bid:args.languages for bid in self.ids}) if args.remove_all: cache.set_field('tags', {bid:() for bid in self.ids}) if args.add or args.remove: self.db.bulk_modify_tags(self.ids, add=args.add, remove=args.remove) if self.do_sr: for book_id in self.ids: self.s_r_func(book_id) if self.sr_calls: for field, book_id_val_map in self.sr_calls.iteritems(): self.refresh_books.update(self.db.new_api.set_field(field, book_id_val_map))
class MyBlockingBusy(QDialog): NORMAL = 0 REQUESTED = 1 ACKNOWLEDGED = 2 def __init__(self, gui, msg, size=100, window_title="Marvin XD", show_cancel=False, on_top=False): flags = Qt.FramelessWindowHint if on_top: flags = Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint QDialog.__init__(self, gui, flags) self._layout = QVBoxLayout() self.setLayout(self._layout) self.cancel_status = 0 self.is_running = False # Add the spinner self.pi = ProgressIndicator(self) self.pi.setDisplaySize(size) self._layout.addSpacing(15) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) # Fiddle with the message self.msg = QLabel(msg) # self.msg.setWordWrap(True) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 2) self.msg.setFont(self.font) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) sp = QSizePolicy() sp.setHorizontalStretch(True) sp.setVerticalStretch(False) sp.setHeightForWidth(False) self.msg.setSizePolicy(sp) self.msg.setMinimumHeight(self.font.pointSize() + 8) self._layout.addSpacing(15) if show_cancel: self.bb = QDialogButtonBox() self.cancel_button = QPushButton(QIcon(I("window-close.png")), "Cancel") self.bb.addButton(self.cancel_button, self.bb.RejectRole) self.bb.clicked.connect(self.button_handler) self._layout.addWidget(self.bb) self.setWindowTitle(window_title) self.resize(self.sizeHint()) def accept(self): self.stop() return QDialog.accept(self) def button_handler(self, button): """ Only change cancel_status from NORMAL to REQUESTED """ if self.bb.buttonRole(button) == QDialogButtonBox.RejectRole: if self.cancel_status == self.NORMAL: self.cancel_status = self.REQUESTED self.cancel_button.setEnabled(False) def reject(self): """ Cannot cancel this dialog manually """ pass def set_text(self, text): self.msg.setText(text) def start(self): self.is_running = True self.pi.startAnimation() def stop(self): self.is_running = False self.pi.stopAnimation()
class BlockingJob(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) l = QVBoxLayout() self.setLayout(l) l.addStretch(10) self.pi = ProgressIndicator(self, 128) l.addWidget(self.pi, alignment=Qt.AlignHCenter) self.dummy = QLabel('<h2>\xa0') l.addSpacing(10) l.addWidget(self.dummy, alignment=Qt.AlignHCenter) l.addStretch(10) self.setVisible(False) self.text = '' self.setFocusPolicy(Qt.NoFocus) def start(self): self.setGeometry(0, 0, self.parent().width(), self.parent().height()) self.setVisible(True) # Prevent any actions from being triggerred by key presses self.parent().setEnabled(False) self.raise_() self.setFocus(Qt.OtherFocusReason) self.pi.startAnimation() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) def stop(self): QApplication.restoreOverrideCursor() self.pi.stopAnimation() self.setVisible(False) self.parent().setEnabled(True) # The following line is needed on OS X, because of this bug: # https://bugreports.qt-project.org/browse/QTBUG-34371 it causes # keyboard events to no longer work self.parent().setFocus(Qt.OtherFocusReason) def job_done(self, callback, job): del job.callback self.stop() callback(job) def paintEvent(self, ev): br = ev.region().boundingRect() p = QPainter(self) p.setOpacity(0.2) p.fillRect(br, QBrush(self.palette().text())) p.end() QWidget.paintEvent(self, ev) p = QPainter(self) p.setClipRect(br) f = p.font() f.setBold(True) f.setPointSize(20) p.setFont(f) p.setPen(Qt.SolidLine) r = QRect(0, self.dummy.geometry().top() + 10, self.geometry().width(), 150) p.drawText(r, Qt.AlignHCenter | Qt.AlignTop | Qt.TextSingleLine, self.text) p.end() def set_msg(self, text): self.text = text def __call__(self, name, user_text, callback, function, *args, **kwargs): ' Run a job that blocks the GUI providing some feedback to the user ' self.set_msg(user_text) job = LongJob(name, user_text, Dispatcher(partial(self.job_done, callback)), function, *args, **kwargs) job.start() self.start()
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._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 + ' [%s]' % self.shortcut) 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:') + ' {} '.format(n)) def event(self, ev): m = None et = ev.type() if et == ev.Enter: m = True elif et == ev.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() def stop(self): self.pi.stopAnimation() 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 = QPainter(self) tool = QStyleOption() tool.rect = self.rect() tool.state = QStyle.StateFlag.State_Raised | QStyle.StateFlag.State_Active | QStyle.StateFlag.State_MouseOver s = self.style() s.drawPrimitive(QStyle.PrimitiveElement.PE_PanelButtonTool, tool, p, self) p.end() QWidget.paintEvent(self, ev)
class BlockingJob(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) l = QVBoxLayout() self.setLayout(l) l.addStretch(10) self.pi = ProgressIndicator(self, 128) l.addWidget(self.pi, alignment=Qt.AlignmentFlag.AlignHCenter) self.dummy = QLabel('<h2>\xa0') l.addSpacing(10) l.addWidget(self.dummy, alignment=Qt.AlignmentFlag.AlignHCenter) l.addStretch(10) self.setVisible(False) self.text = '' self.setFocusPolicy(Qt.FocusPolicy.NoFocus) def start(self): self.setGeometry(0, 0, self.parent().width(), self.parent().height()) self.setVisible(True) # Prevent any actions from being triggered by key presses self.parent().setEnabled(False) self.raise_() self.setFocus(Qt.FocusReason.OtherFocusReason) self.pi.startAnimation() QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) def stop(self): QApplication.restoreOverrideCursor() self.pi.stopAnimation() self.setVisible(False) self.parent().setEnabled(True) # The following line is needed on OS X, because of this bug: # https://bugreports.qt-project.org/browse/QTBUG-34371 it causes # keyboard events to no longer work self.parent().setFocus(Qt.FocusReason.OtherFocusReason) def job_done(self, callback, job): del job.callback self.stop() callback(job) def paintEvent(self, ev): br = ev.region().boundingRect() p = QPainter(self) p.setOpacity(0.2) p.fillRect(br, QBrush(self.palette().text())) p.end() QWidget.paintEvent(self, ev) p = QPainter(self) p.setClipRect(br) f = p.font() f.setBold(True) f.setPointSize(20) p.setFont(f) p.setPen(Qt.PenStyle.SolidLine) r = QRect(0, self.dummy.geometry().top() + 10, self.geometry().width(), 150) p.drawText( r, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop | Qt.TextFlag.TextSingleLine, self.text) p.end() def set_msg(self, text): self.text = text def __call__(self, name, user_text, callback, function, *args, **kwargs): ' Run a job that blocks the GUI providing some feedback to the user ' self.set_msg(user_text) job = LongJob(name, user_text, Dispatcher(partial(self.job_done, callback)), function, *args, **kwargs) job.start() self.start()
class JobsButton(QFrame): # {{{ def __init__(self, horizontal=False, size=48, parent=None): QFrame.__init__(self, parent) if horizontal: size = 24 self.pi = ProgressIndicator(self, size) self._jobs = QLabel('<b>'+_('Jobs:')+' 0') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent self.shortcut = _('Shift+Alt+J') if horizontal: self.setLayout(QHBoxLayout()) self.layout().setDirection(self.layout().RightToLeft) else: self.setLayout(QVBoxLayout()) self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom) self.layout().addWidget(self.pi) self.layout().addWidget(self._jobs) if not horizontal: self.layout().setAlignment(self._jobs, Qt.AlignHCenter) self._jobs.setMargin(0) self.layout().setMargin(0) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setCursor(Qt.PointingHandCursor) b = _('Click to see list of jobs') self.setToolTip(b + u' (%s)'%self.shortcut) self.action_toggle = QAction(b, parent) parent.addAction(self.action_toggle) self.action_toggle.setShortcut(self.shortcut) self.action_toggle.triggered.connect(self.toggle) 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() def stop(self): self.pi.stopAnimation() def jobs(self): src = unicode(self._jobs.text()) return int(re.search(r'\d+', src).group()) def job_added(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) self.start() def job_done(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) if nnum == 0: self.no_more_jobs() def no_more_jobs(self): if self.is_running: self.stop() QCoreApplication.instance().alert(self, 5000)
class BlockingJob(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) l = QVBoxLayout() self.setLayout(l) l.addStretch(10) self.pi = ProgressIndicator(self, 128) l.addWidget(self.pi, alignment=Qt.AlignHCenter) self.dummy = QLabel('<h2>\xa0') l.addSpacing(10) l.addWidget(self.dummy, alignment=Qt.AlignHCenter) l.addStretch(10) self.setVisible(False) self.text = '' def start(self): self.setGeometry(0, 0, self.parent().width(), self.parent().height()) self.setVisible(True) # Prevent any actions from being triggerred by key presses self.parent().setEnabled(False) self.raise_() self.setFocus(Qt.OtherFocusReason) self.pi.startAnimation() def stop(self): self.pi.stopAnimation() self.setVisible(False) self.parent().setEnabled(True) def job_done(self, callback, job): del job.callback self.stop() callback(job) def paintEvent(self, ev): br = ev.region().boundingRect() p = QPainter(self) p.setOpacity(0.2) p.fillRect(br, QBrush(self.palette().text())) p.end() QWidget.paintEvent(self, ev) p = QPainter(self) p.setClipRect(br) f = p.font() f.setBold(True) f.setPointSize(20) p.setFont(f) p.setPen(Qt.SolidLine) r = QRect(0, self.dummy.geometry().top() + 10, self.geometry().width(), 150) p.drawText(r, Qt.AlignHCenter | Qt.AlignTop | Qt.TextSingleLine, self.text) p.end() def eventFilter(self, obj, ev): if ev.type() in (ev.KeyPress, ev.KeyRelease): return True return QWidget.eventFilter(self, obj, ev) def set_msg(self, text): self.text = text def __call__(self, name, user_text, callback, function, *args, **kwargs): ' Run a job that blocks the GUI providing some feedback to the user ' self.set_msg(user_text) job = LongJob(name, user_text, Dispatcher(partial(self.job_done, callback)), function, *args, **kwargs) job.start() self.start()
class SearchDialog(QDialog, Ui_Dialog): def __init__(self, gui, parent=None, query=''): QDialog.__init__(self, parent) self.setupUi(self) self.config = JSONConfig('store/search') self.search_title.initialize('store_search_search_title') self.search_author.initialize('store_search_search_author') self.search_edit.initialize('store_search_search') # Loads variables that store various settings. # This needs to be called soon in __init__ because # the variables it sets up are used later. self.load_settings() self.gui = gui # Setup our worker threads. self.search_pool = SearchThreadPool(self.search_thread_count) self.cache_pool = CacheUpdateThreadPool(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count(self.cover_thread_count) self.results_view.model().details_pool.set_thread_count(self.details_thread_count) self.results_view.setCursor(Qt.PointingHandCursor) # Check for results and hung threads. self.checker = QTimer() self.progress_checker = QTimer() self.hang_check = 0 # Update store caches silently. for p in self.gui.istores.values(): self.cache_pool.add_task(p, self.timeout) self.store_checks = {} self.setup_store_checks() # Set the search query if isinstance(query, (str, unicode)): self.search_edit.setText(query) elif isinstance(query, dict): if 'author' in query: self.search_author.setText(query['author']) if 'title' in query: self.search_title.setText(query['title']) # Title self.search_title.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) self.search_title.setMinimumContentsLength(25) # Author self.search_author.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) self.search_author.setMinimumContentsLength(25) # Keyword self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon) self.search_edit.setMinimumContentsLength(25) # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) self.button_layout.takeAt(0) self.button_layout.setAlignment(Qt.AlignCenter) self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignCenter) self.adv_search_button.setIcon(QIcon(I('search.png'))) self.configure.setIcon(QIcon(I('config.png'))) self.adv_search_button.clicked.connect(self.build_adv_search) self.search.clicked.connect(self.do_search) self.checker.timeout.connect(self.get_results) self.progress_checker.timeout.connect(self.check_progress) self.results_view.activated.connect(self.result_item_activated) self.results_view.download_requested.connect(self.download_book) self.results_view.open_requested.connect(self.open_store) self.results_view.model().total_changed.connect(self.update_book_total) self.select_all_stores.clicked.connect(self.stores_select_all) self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_none_stores.clicked.connect(self.stores_select_none) self.configure.clicked.connect(self.do_config) self.finished.connect(self.dialog_closed) self.progress_checker.start(100) self.restore_state() def setup_store_checks(self): first_run = self.config.get('first_run', True) # Add check boxes for each store so the user # can disable searching specific stores on a # per search basis. existing = {} for n in self.store_checks: existing[n] = self.store_checks[n].isChecked() self.store_checks = {} stores_check_widget = QWidget() store_list_layout = QGridLayout() stores_check_widget.setLayout(store_list_layout) icon = QIcon(I('donate.png')) for i, x in enumerate(sorted(self.gui.istores.keys(), key=lambda x: x.lower())): cbox = QCheckBox(x) cbox.setChecked(existing.get(x, first_run)) store_list_layout.addWidget(cbox, i, 0, 1, 1) if self.gui.istores[x].base_plugin.affiliate: iw = QLabel(self) iw.setToolTip('<p>' + _('Buying from this store supports the calibre developer: %s</p>') % self.gui.istores[x].base_plugin.author + '</p>') iw.setPixmap(icon.pixmap(16, 16)) store_list_layout.addWidget(iw, i, 1, 1, 1) self.store_checks[x] = cbox store_list_layout.setRowStretch(store_list_layout.rowCount(), 10) self.store_list.setWidget(stores_check_widget) self.config['first_run'] = False def build_adv_search(self): adv = AdvSearchBuilderDialog(self) if adv.exec_() == QDialog.Accepted: self.search_edit.setText(adv.search_string()) def resize_columns(self): total = 600 # Cover self.results_view.setColumnWidth(0, 85) total = total - 85 # Title / Author self.results_view.setColumnWidth(1,int(total*.40)) # Price self.results_view.setColumnWidth(2,int(total*.12)) # DRM self.results_view.setColumnWidth(3, int(total*.15)) # Store / Formats self.results_view.setColumnWidth(4, int(total*.25)) # Download self.results_view.setColumnWidth(5, 20) # Affiliate self.results_view.setColumnWidth(6, 20) def do_search(self): # Stop all running threads. self.checker.stop() self.search_pool.abort() # Clear the visible results. self.results_view.model().clear_results() # Don't start a search if there is nothing to search for. query = [] if self.search_title.text(): query.append(u'title2:"~%s"' % unicode(self.search_title.text()).replace('"', ' ')) if self.search_author.text(): query.append(u'author2:"%s"' % unicode(self.search_author.text()).replace('"', ' ')) if self.search_edit.text(): query.append(unicode(self.search_edit.text())) query = " ".join(query) if not query.strip(): error_dialog(self, _('No query'), _('You must enter a title, author or keyword to' ' search for.'), show=True) return # Give the query to the results model so it can do # futher filtering. self.results_view.model().set_query(query) # Plugins are in random order that does not change. # Randomize the ord of the plugin names every time # there is a search. This way plugins closer # to a don't have an unfair advantage over # plugins further from a. store_names = self.store_checks.keys() if not store_names: return # Remove all of our internal filtering logic from the query. query = self.clean_query(query) shuffle(store_names) # Add plugins that the user has checked to the search pool's work queue. self.gui.istores.join(4.0) # Wait for updated plugins to load for n in store_names: if self.store_checks[n].isChecked(): self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout) self.hang_check = 0 self.checker.start(100) self.pi.startAnimation() def clean_query(self, query): query = query.lower() # Remove control modifiers. query = query.replace('\\', '') query = query.replace('!', '') query = query.replace('=', '') query = query.replace('~', '') query = query.replace('>', '') query = query.replace('<', '') # Remove the prefix. for loc in ('all', 'author', 'author2', 'authors', 'title', 'title2'): query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove logic. query = re.sub(r'(^|\s)(and|not|or|a|the|is|of)(\s|$)', ' ', query) # Remove " query = query.replace('"', '') # Remove excess whitespace. query = re.sub(r'\s{2,}', ' ', query) query = query.strip() return query.encode('utf-8') def save_state(self): self.config['geometry'] = bytearray(self.saveGeometry()) self.config['store_splitter_state'] = bytearray(self.store_splitter.saveState()) self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] self.config['sort_col'] = self.results_view.model().sort_col self.config['sort_order'] = self.results_view.model().sort_order self.config['open_external'] = self.open_external.isChecked() store_check = {} for k, v in self.store_checks.items(): store_check[k] = v.isChecked() self.config['store_checked'] = store_check def restore_state(self): geometry = self.config.get('geometry', None) if geometry: self.restoreGeometry(geometry) splitter_state = self.config.get('store_splitter_state', None) if splitter_state: self.store_splitter.restoreState(splitter_state) results_cwidth = self.config.get('results_view_column_width', None) if results_cwidth: for i, x in enumerate(results_cwidth): if i >= self.results_view.model().columnCount(): break self.results_view.setColumnWidth(i, x) else: self.resize_columns() self.open_external.setChecked(self.should_open_external) store_check = self.config.get('store_checked', None) if store_check: for n in store_check: if n in self.store_checks: self.store_checks[n].setChecked(store_check[n]) self.results_view.model().sort_col = self.config.get('sort_col', 2) self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder) self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order) def load_settings(self): # Seconds self.timeout = self.config.get('timeout', 75) # Milliseconds self.hang_time = self.config.get('hang_time', 75) * 1000 self.max_results = self.config.get('max_results', 15) self.should_open_external = self.config.get('open_external', True) # Number of threads to run for each type of operation self.search_thread_count = self.config.get('search_thread_count', 4) self.cache_thread_count = self.config.get('cache_thread_count', 2) self.cover_thread_count = self.config.get('cover_thread_count', 2) self.details_thread_count = self.config.get('details_thread_count', 4) def do_config(self): # Save values that need to be synced between the dialog and the # search widget. self.config['open_external'] = self.open_external.isChecked() # Create the config dialog. It's going to put two config widgets # into a QTabWidget for displaying all of the settings. d = QDialog(self) button_box = QDialogButtonBox(QDialogButtonBox.Close) v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize get books search')) tab_widget = QTabWidget(d) v.addWidget(tab_widget) v.addWidget(button_box) chooser_config_widget = StoreChooserWidget() search_config_widget = StoreConfigWidget(self.config) tab_widget.addTab(chooser_config_widget, _('Choose stores')) tab_widget.addTab(search_config_widget, _('Configure search')) # Restore dialog state. geometry = self.config.get('config_dialog_geometry', None) if geometry: d.restoreGeometry(geometry) else: d.resize(800, 600) tab_index = self.config.get('config_dialog_tab_index', 0) tab_index = min(tab_index, tab_widget.count() - 1) tab_widget.setCurrentIndex(tab_index) d.exec_() # Save dialog state. self.config['config_dialog_geometry'] = bytearray(d.saveGeometry()) self.config['config_dialog_tab_index'] = tab_widget.currentIndex() search_config_widget.save_settings() self.config_changed() self.gui.load_store_plugins() self.setup_store_checks() def config_changed(self): self.load_settings() self.open_external.setChecked(self.should_open_external) self.search_pool.set_thread_count(self.search_thread_count) self.cache_pool.set_thread_count(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count(self.cover_thread_count) self.results_view.model().details_pool.set_thread_count(self.details_thread_count) def get_results(self): # We only want the search plugins to run # a maximum set amount of time before giving up. self.hang_check += 1 if self.hang_check >= self.hang_time: self.search_pool.abort() self.checker.stop() else: # Stop the checker if not threads are running. if not self.search_pool.threads_running() and not self.search_pool.has_tasks(): self.checker.stop() while self.search_pool.has_results(): res, store_plugin = self.search_pool.get_result() if res: self.results_view.model().add_result(res, store_plugin) if not self.search_pool.threads_running() and not self.results_view.model().has_results(): info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) def update_book_total(self, total): self.total.setText('%s' % total) def result_item_activated(self, index): result = self.results_view.model().get_result(index) if result.downloads: self.download_book(result) else: self.open_store(result) def download_book(self, result): d = ChooseFormatDialog(self, _('Choose format to download to your library.'), result.downloads.keys()) if d.exec_() == d.Accepted: ext = d.format() fname = result.title[:60] + '.' + ext.lower() fname = ascii_filename(fname) self.gui.download_ebook(result.downloads[ext], filename=fname) def open_store(self, result): self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) def check_progress(self): if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): self.pi.stopAnimation() else: if not self.pi.isAnimated(): self.pi.startAnimation() def stores_select_all(self): for check in self.store_checks.values(): check.setChecked(True) def stores_select_invert(self): for check in self.store_checks.values(): check.setChecked(not check.isChecked()) def stores_select_none(self): for check in self.store_checks.values(): check.setChecked(False) def dialog_closed(self, result): self.results_view.model().closing() self.search_pool.abort() self.cache_pool.abort() self.save_state() def exec_(self): if unicode(self.search_edit.text()).strip() or unicode(self.search_title.text()).strip() or unicode(self.search_author.text()).strip(): self.do_search() return QDialog.exec_(self)
class JobsButton(QFrame): # {{{ tray_tooltip_updated = pyqtSignal(object) def __init__(self, horizontal=False, size=48, parent=None): QFrame.__init__(self, parent) if horizontal: size = 24 self.pi = ProgressIndicator(self, size) self._jobs = QLabel('<b>' + _('Jobs:') + ' 0') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent self.shortcut = 'Shift+Alt+J' if horizontal: self.setLayout(QHBoxLayout()) self.layout().setDirection(self.layout().RightToLeft) else: self.setLayout(QVBoxLayout()) self._jobs.setAlignment(Qt.AlignHCenter | Qt.AlignBottom) self.layout().addWidget(self.pi) self.layout().addWidget(self._jobs) if not horizontal: self.layout().setAlignment(self._jobs, Qt.AlignHCenter) self._jobs.setMargin(0) self.layout().setContentsMargins(0, 0, 0, 0) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setCursor(Qt.PointingHandCursor) b = _('Click to see list of jobs') self.setToolTip(b + u' (%s)' % self.shortcut) 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 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() def stop(self): self.pi.stopAnimation() def jobs(self): src = unicode(self._jobs.text()) return int(re.search(r'\d+', src).group()) 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): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) self.start() self.tray_tooltip_updated.emit(self.tray_tooltip(nnum)) def job_done(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) 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)
class JobsButton(QWidget): # {{{ tray_tooltip_updated = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) self.mouse_over = False self.pi = ProgressIndicator(self, self.style().pixelMetric(QStyle.PM_ToolBarIconSize)) self._jobs = QLabel('<b>'+_('Jobs:')+' 0 ') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent 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.PM_DefaultFrameWidth) self.layout().setContentsMargins(m, m, m, m) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setCursor(Qt.PointingHandCursor) b = _('Click to see list of jobs') self.setToolTip(b + u' [%s]'%self.shortcut) 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 event(self, ev): m = None et = ev.type() if et == ev.Enter: m = True elif et == ev.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() def stop(self): self.pi.stopAnimation() def jobs(self): src = unicode(self._jobs.text()) return int(re.search(r'\d+', src).group()) 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): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) self.start() self.tray_tooltip_updated.emit(self.tray_tooltip(nnum)) def job_done(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) 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 = QPainter(self) tool = QStyleOption() tool.rect = self.rect() tool.state = QStyle.State_Raised | QStyle.State_Active | QStyle.State_MouseOver s = self.style() s.drawPrimitive(QStyle.PE_PanelButtonTool, tool, p, self) p.end() QWidget.paintEvent(self, ev)
class JobsButton(QFrame): # {{{ tray_tooltip_updated = pyqtSignal(object) def __init__(self, horizontal=False, size=48, parent=None): QFrame.__init__(self, parent) if horizontal: size = 24 self.pi = ProgressIndicator(self, size) self._jobs = QLabel('<b>'+_('Jobs:')+' 0') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent self.shortcut = 'Shift+Alt+J' if horizontal: self.setLayout(QHBoxLayout()) self.layout().setDirection(self.layout().RightToLeft) else: self.setLayout(QVBoxLayout()) self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom) self.layout().addWidget(self.pi) self.layout().addWidget(self._jobs) if not horizontal: self.layout().setAlignment(self._jobs, Qt.AlignHCenter) self._jobs.setMargin(0) self.layout().setContentsMargins(0, 0, 0, 0) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setCursor(Qt.PointingHandCursor) b = _('Click to see list of jobs') self.setToolTip(b + u' (%s)'%self.shortcut) 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 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() def stop(self): self.pi.stopAnimation() def jobs(self): src = unicode(self._jobs.text()) return int(re.search(r'\d+', src).group()) 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): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) self.start() self.tray_tooltip_updated.emit(self.tray_tooltip(nnum)) def job_done(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) 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)
class JobsButton(QFrame): # {{{ def __init__(self, horizontal=False, size=48, parent=None): QFrame.__init__(self, parent) if horizontal: size = 24 self.pi = ProgressIndicator(self, size) self._jobs = QLabel('<b>'+_('Jobs:')+' 0') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent self.shortcut = 'Shift+Alt+J' if horizontal: self.setLayout(QHBoxLayout()) self.layout().setDirection(self.layout().RightToLeft) else: self.setLayout(QVBoxLayout()) self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom) self.layout().addWidget(self.pi) self.layout().addWidget(self._jobs) if not horizontal: self.layout().setAlignment(self._jobs, Qt.AlignHCenter) self._jobs.setMargin(0) self.layout().setContentsMargins(0, 0, 0, 0) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setCursor(Qt.PointingHandCursor) b = _('Click to see list of jobs') self.setToolTip(b + u' (%s)'%self.shortcut) self.action_toggle = QAction(b, parent) parent.addAction(self.action_toggle) self.action_toggle.setShortcut(self.shortcut) self.action_toggle.triggered.connect(self.toggle) 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() def stop(self): self.pi.stopAnimation() def jobs(self): src = unicode(self._jobs.text()) return int(re.search(r'\d+', src).group()) def job_added(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) self.start() def job_done(self, nnum): jobs = self._jobs src = unicode(jobs.text()) num = self.jobs() text = src.replace(str(num), str(nnum)) jobs.setText(text) if nnum == 0: self.no_more_jobs() def no_more_jobs(self): if self.is_running: self.stop() QCoreApplication.instance().alert(self, 5000)
class MyBlockingBusy(QDialog): # {{{ do_one_signal = pyqtSignal() phases = ['', _('Title/Author'), _('Standard metadata'), _('Custom metadata'), _('Search/Replace'), ] def __init__(self, msg, args, db, ids, cc_widgets, s_r_func, parent=None, window_title=_('Working')): QDialog.__init__(self, parent) self._layout = QVBoxLayout() self.setLayout(self._layout) self.msg_text = msg self.msg = QLabel(msg+' ') # Ensure dialog is wide enough #self.msg.setWordWrap(True) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) self.pi = ProgressIndicator(self) self.pi.setDisplaySize(100) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.setWindowTitle(window_title) self.resize(self.sizeHint()) self.start() self.args = args self.series_start_value = None self.db = db self.ids = ids self.error = None self.cc_widgets = cc_widgets self.s_r_func = s_r_func self.do_one_signal.connect(self.do_one_safe, Qt.QueuedConnection) def start(self): self.pi.startAnimation() def stop(self): self.pi.stopAnimation() def accept(self): self.stop() return QDialog.accept(self) def exec_(self): self.current_index = 0 self.current_phase = 1 self.do_one_signal.emit() return QDialog.exec_(self) def do_one_safe(self): try: if self.current_index >= len(self.ids): self.current_phase += 1 self.current_index = 0 if self.current_phase > 4: self.db.commit() return self.accept() id = self.ids[self.current_index] percent = int((self.current_index*100)/float(len(self.ids))) self.msg.setText(self.msg_text.format(self.phases[self.current_phase], percent)) self.do_one(id) except Exception as err: import traceback try: err = unicode(err) except: err = repr(err) self.error = (err, traceback.format_exc()) return self.accept() def do_one(self, id): remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_remove_conv, do_auto_author, series, do_series_restart, \ series_start_value, do_title_case, cover_action, clear_series, \ pubdate, adddate, do_title_sort, languages, clear_languages, \ restore_original = self.args # first loop: All changes that modify the filesystem and commit # immediately. We want to # try hard to keep the DB and the file system in sync, even in the face # of exceptions or forced exits. if self.current_phase == 1: title_set = False if do_swap_ta: title = self.db.title(id, index_is_id=True) aum = self.db.authors(id, index_is_id=True) if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')] new_title = authors_to_string(aum) if do_title_case: new_title = titlecase(new_title) self.db.set_title(id, new_title, notify=False) title_set = True if title: new_authors = string_to_authors(title) self.db.set_authors(id, new_authors, notify=False) if do_title_case and not title_set: title = self.db.title(id, index_is_id=True) self.db.set_title(id, titlecase(title), notify=False) if do_title_sort: title = self.db.title(id, index_is_id=True) if languages: lang = languages[0] else: lang = self.db.languages(id, index_is_id=True) if lang: lang = lang.partition(',')[0] self.db.set_title_sort(id, title_sort(title, lang=lang), notify=False) if au: self.db.set_authors(id, string_to_authors(au), notify=False) if cover_action == 'remove': self.db.remove_cover(id) elif cover_action == 'generate': from calibre.ebooks import calibre_cover from calibre.ebooks.metadata import fmt_sidx from calibre.gui2 import config mi = self.db.get_metadata(id, index_is_id=True) series_string = None if mi.series: series_string = _('Book %(sidx)s of %(series)s')%dict( sidx=fmt_sidx(mi.series_index, use_roman=config['use_roman_numerals_for_series_number']), series=mi.series) cdata = calibre_cover(mi.title, mi.format_field('authors')[-1], series_string=series_string) self.db.set_cover(id, cdata) elif cover_action == 'fromfmt': fmts = self.db.formats(id, index_is_id=True, verify_formats=False) if fmts: covers = [] for fmt in fmts.split(','): fmtf = self.db.format(id, fmt, index_is_id=True, as_file=True) if fmtf is None: continue cdata, area = get_cover_data(fmtf, fmt) if cdata: covers.append((cdata, area)) covers.sort(key=lambda x: x[1]) if covers: self.db.set_cover(id, covers[-1][0]) covers = [] if do_remove_format: self.db.remove_format(id, remove_format, index_is_id=True, notify=False, commit=True) if restore_original: formats = self.db.formats(id, index_is_id=True) formats = formats.split(',') if formats else [] originals = [x.upper() for x in formats if x.upper().startswith('ORIGINAL_')] for ofmt in originals: fmt = ofmt.replace('ORIGINAL_', '') with SpooledTemporaryFile(SPOOL_SIZE) as stream: self.db.copy_format_to(id, ofmt, stream, index_is_id=True) stream.seek(0) self.db.add_format(id, fmt, stream, index_is_id=True, notify=False) self.db.remove_format(id, ofmt, index_is_id=True, notify=False, commit=True) elif self.current_phase == 2: # All of these just affect the DB, so we can tolerate a total rollback if do_auto_author: x = self.db.author_sort_from_book(id, index_is_id=True) if x: self.db.set_author_sort(id, x, notify=False, commit=False) if aus and do_aus: self.db.set_author_sort(id, aus, notify=False, commit=False) if rating != -1: self.db.set_rating(id, 2*rating, notify=False, commit=False) if pub: self.db.set_publisher(id, pub, notify=False, commit=False) if clear_series: self.db.set_series(id, '', notify=False, commit=False) if pubdate is not None: self.db.set_pubdate(id, pubdate, notify=False, commit=False) if adddate is not None: self.db.set_timestamp(id, adddate, notify=False, commit=False) if do_series: if do_series_restart: if self.series_start_value is None: self.series_start_value = series_start_value next = self.series_start_value self.series_start_value += 1 else: next = self.db.get_next_series_num_for(series) self.db.set_series(id, series, notify=False, commit=False) if not series: self.db.set_series_index(id, 1.0, notify=False, commit=False) elif do_autonumber: # is True if do_series_restart is True self.db.set_series_index(id, next, notify=False, commit=False) elif tweaks['series_index_auto_increment'] != 'no_change': self.db.set_series_index(id, 1.0, notify=False, commit=False) if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE', commit=False) if clear_languages: self.db.set_languages(id, [], notify=False, commit=False) elif languages: self.db.set_languages(id, languages, notify=False, commit=False) elif self.current_phase == 3: # both of these are fast enough to just do them all for w in self.cc_widgets: w.commit(self.ids) if remove_all: self.db.remove_all_tags(self.ids) self.db.bulk_modify_tags(self.ids, add=add, remove=remove, notify=False) self.current_index = len(self.ids) elif self.current_phase == 4: self.s_r_func(id) # do the next one self.current_index += 1 self.do_one_signal.emit()
class SearchDialog(QDialog, Ui_Dialog): SEARCH_TEXT = _('&Search') STOP_TEXT = _('&Stop') def __init__(self, gui, parent=None, query=''): QDialog.__init__(self, parent) self.setupUi(self) s = self.style() self.close.setIcon(s.standardIcon(s.SP_DialogCloseButton)) self.config = JSONConfig('store/search') self.search_title.initialize('store_search_search_title') self.search_author.initialize('store_search_search_author') self.search_edit.initialize('store_search_search') # Loads variables that store various settings. # This needs to be called soon in __init__ because # the variables it sets up are used later. self.load_settings() self.gui = gui # Setup our worker threads. self.search_pool = SearchThreadPool(self.search_thread_count) self.cache_pool = CacheUpdateThreadPool(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count( self.cover_thread_count) self.results_view.model().details_pool.set_thread_count( self.details_thread_count) self.results_view.setCursor(Qt.PointingHandCursor) # Check for results and hung threads. self.checker = QTimer() self.progress_checker = QTimer() self.hang_check = 0 # Update store caches silently. for p in self.gui.istores.values(): self.cache_pool.add_task(p, self.timeout) self.store_checks = {} self.setup_store_checks() # Set the search query if isinstance(query, (bytes, unicode_type)): self.search_edit.setText(query) elif isinstance(query, dict): if 'author' in query: self.search_author.setText(query['author']) if 'title' in query: self.search_title.setText(query['title']) # Create and add the progress indicator self.pi = ProgressIndicator(self, 24) self.button_layout.takeAt(0) self.button_layout.setAlignment(Qt.AlignCenter) self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignCenter) self.adv_search_button.setIcon(QIcon(I('gear.png'))) self.adv_search_button.setToolTip(_('Advanced search')) self.configure.setIcon(QIcon(I('config.png'))) self.adv_search_button.clicked.connect(self.build_adv_search) self.search.clicked.connect(self.toggle_search) self.checker.timeout.connect(self.get_results) self.progress_checker.timeout.connect(self.check_progress) self.results_view.activated.connect(self.result_item_activated) self.results_view.download_requested.connect(self.download_book) self.results_view.open_requested.connect(self.open_store) self.results_view.model().total_changed.connect(self.update_book_total) self.select_all_stores.clicked.connect(self.stores_select_all) self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_none_stores.clicked.connect(self.stores_select_none) self.configure.clicked.connect(self.do_config) self.finished.connect(self.dialog_closed) self.searching = False self.progress_checker.start(100) self.restore_state() def setup_store_checks(self): first_run = self.config.get('first_run', True) # Add check boxes for each store so the user # can disable searching specific stores on a # per search basis. existing = {} for n in self.store_checks: existing[n] = self.store_checks[n].isChecked() self.store_checks = {} stores_check_widget = QWidget() store_list_layout = QGridLayout() stores_check_widget.setLayout(store_list_layout) icon = QIcon(I('donate.png')) for i, x in enumerate( sorted(self.gui.istores.keys(), key=lambda x: x.lower())): cbox = QCheckBox(x) cbox.setChecked(existing.get(x, first_run)) store_list_layout.addWidget(cbox, i, 0, 1, 1) if self.gui.istores[x].base_plugin.affiliate: iw = QLabel(self) iw.setToolTip('<p>' + _( 'Buying from this store supports the calibre developer: %s</p>' ) % self.gui.istores[x].base_plugin.author + '</p>') iw.setPixmap(icon.pixmap(16, 16)) store_list_layout.addWidget(iw, i, 1, 1, 1) self.store_checks[x] = cbox store_list_layout.setRowStretch(store_list_layout.rowCount(), 10) self.store_list.setWidget(stores_check_widget) self.config['first_run'] = False def build_adv_search(self): adv = AdvSearchBuilderDialog(self) if adv.exec_() == QDialog.Accepted: self.search_edit.setText(adv.search_string()) def resize_columns(self): total = 600 # Cover self.results_view.setColumnWidth(0, 85) total = total - 85 # Title / Author self.results_view.setColumnWidth(1, int(total * .40)) # Price self.results_view.setColumnWidth(2, int(total * .12)) # DRM self.results_view.setColumnWidth(3, int(total * .15)) # Store / Formats self.results_view.setColumnWidth(4, int(total * .25)) # Download self.results_view.setColumnWidth(5, 20) # Affiliate self.results_view.setColumnWidth(6, 20) def toggle_search(self): if self.searching: self.search_pool.abort() m = self.results_view.model() m.details_pool.abort() m.cover_pool.abort() self.search.setText(self.SEARCH_TEXT) self.checker.stop() self.searching = False else: self.do_search() # Prevent hitting the enter key twice in quick succession causing # the search to start and stop self.search.setEnabled(False) QTimer.singleShot(1000, lambda: self.search.setEnabled(True)) def do_search(self): # Stop all running threads. self.checker.stop() self.search_pool.abort() # Clear the visible results. self.results_view.model().clear_results() # Don't start a search if there is nothing to search for. query = [] if self.search_title.text(): query.append( u'title2:"~%s"' % unicode_type(self.search_title.text()).replace('"', ' ')) if self.search_author.text(): query.append( u'author2:"%s"' % unicode_type(self.search_author.text()).replace('"', ' ')) if self.search_edit.text(): query.append(unicode_type(self.search_edit.text())) query = " ".join(query) if not query.strip(): error_dialog(self, _('No query'), _('You must enter a title, author or keyword to' ' search for.'), show=True) return self.searching = True self.search.setText(self.STOP_TEXT) # Give the query to the results model so it can do # futher filtering. self.results_view.model().set_query(query) # Plugins are in random order that does not change. # Randomize the ord of the plugin names every time # there is a search. This way plugins closer # to a don't have an unfair advantage over # plugins further from a. store_names = list(self.store_checks) if not store_names: return # Remove all of our internal filtering logic from the query. query = self.clean_query(query) shuffle(store_names) # Add plugins that the user has checked to the search pool's work queue. self.gui.istores.join(4.0) # Wait for updated plugins to load for n in store_names: if self.store_checks[n].isChecked(): self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout) self.hang_check = 0 self.checker.start(100) self.pi.startAnimation() def clean_query(self, query): query = query.lower() # Remove control modifiers. query = query.replace('\\', '') query = query.replace('!', '') query = query.replace('=', '') query = query.replace('~', '') query = query.replace('>', '') query = query.replace('<', '') # Remove the prefix. for loc in ('all', 'author', 'author2', 'authors', 'title', 'title2'): query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, r'\g<a>', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove logic. query = re.sub(r'(^|\s|")(and|not|or|a|the|is|of)(\s|$|")', r' ', query) # Remove " query = query.replace('"', '') # Remove excess whitespace. query = re.sub(r'\s+', ' ', query) query = query.strip() return query.encode('utf-8') def save_state(self): self.config['geometry'] = bytearray(self.saveGeometry()) self.config['store_splitter_state'] = bytearray( self.store_splitter.saveState()) self.config['results_view_column_width'] = [ self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount()) ] self.config['sort_col'] = self.results_view.model().sort_col self.config['sort_order'] = self.results_view.model().sort_order self.config['open_external'] = self.open_external.isChecked() store_check = {} for k, v in self.store_checks.items(): store_check[k] = v.isChecked() self.config['store_checked'] = store_check def restore_state(self): geometry = self.config.get('geometry', None) if geometry: QApplication.instance().safe_restore_geometry(self, geometry) splitter_state = self.config.get('store_splitter_state', None) if splitter_state: self.store_splitter.restoreState(splitter_state) results_cwidth = self.config.get('results_view_column_width', None) if results_cwidth: for i, x in enumerate(results_cwidth): if i >= self.results_view.model().columnCount(): break self.results_view.setColumnWidth(i, x) else: self.resize_columns() self.open_external.setChecked(self.should_open_external) store_check = self.config.get('store_checked', None) if store_check: for n in store_check: if n in self.store_checks: self.store_checks[n].setChecked(store_check[n]) self.results_view.model().sort_col = self.config.get('sort_col', 2) self.results_view.model().sort_order = self.config.get( 'sort_order', Qt.AscendingOrder) self.results_view.header().setSortIndicator( self.results_view.model().sort_col, self.results_view.model().sort_order) def load_settings(self): # Seconds self.timeout = self.config.get('timeout', 75) # Milliseconds self.hang_time = self.config.get('hang_time', 75) * 1000 self.max_results = self.config.get('max_results', 15) self.should_open_external = self.config.get('open_external', True) # Number of threads to run for each type of operation self.search_thread_count = self.config.get('search_thread_count', 4) self.cache_thread_count = self.config.get('cache_thread_count', 2) self.cover_thread_count = self.config.get('cover_thread_count', 2) self.details_thread_count = self.config.get('details_thread_count', 4) def do_config(self): # Save values that need to be synced between the dialog and the # search widget. self.config['open_external'] = self.open_external.isChecked() # Create the config dialog. It's going to put two config widgets # into a QTabWidget for displaying all of the settings. d = QDialog(self) button_box = QDialogButtonBox(QDialogButtonBox.Close) v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize Get books search')) tab_widget = QTabWidget(d) v.addWidget(tab_widget) v.addWidget(button_box) chooser_config_widget = StoreChooserWidget() search_config_widget = StoreConfigWidget(self.config) tab_widget.addTab(chooser_config_widget, _('Choose s&tores')) tab_widget.addTab(search_config_widget, _('Configure s&earch')) # Restore dialog state. geometry = self.config.get('config_dialog_geometry', None) if geometry: QApplication.instance().safe_restore_geometry(d, geometry) else: d.resize(800, 600) tab_index = self.config.get('config_dialog_tab_index', 0) tab_index = min(tab_index, tab_widget.count() - 1) tab_widget.setCurrentIndex(tab_index) d.exec_() # Save dialog state. self.config['config_dialog_geometry'] = bytearray(d.saveGeometry()) self.config['config_dialog_tab_index'] = tab_widget.currentIndex() search_config_widget.save_settings() self.config_changed() self.gui.load_store_plugins() self.setup_store_checks() def config_changed(self): self.load_settings() self.open_external.setChecked(self.should_open_external) self.search_pool.set_thread_count(self.search_thread_count) self.cache_pool.set_thread_count(self.cache_thread_count) self.results_view.model().cover_pool.set_thread_count( self.cover_thread_count) self.results_view.model().details_pool.set_thread_count( self.details_thread_count) def get_results(self): # We only want the search plugins to run # a maximum set amount of time before giving up. self.hang_check += 1 if self.hang_check >= self.hang_time: self.search_pool.abort() self.checker.stop() else: # Stop the checker if not threads are running. if not self.search_pool.threads_running( ) and not self.search_pool.has_tasks(): self.checker.stop() while self.search_pool.has_results(): res, store_plugin = self.search_pool.get_result() if res: self.results_view.model().add_result(res, store_plugin) if not self.search_pool.threads_running( ) and not self.results_view.model().has_results(): info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) def update_book_total(self, total): self.total.setText('%s' % total) def result_item_activated(self, index): result = self.results_view.model().get_result(index) if result.downloads: self.download_book(result) else: self.open_store(result) def download_book(self, result): d = ChooseFormatDialog(self, _('Choose format to download to your library.'), list(result.downloads.keys())) if d.exec_() == d.Accepted: ext = d.format() fname = result.title[:60] + '.' + ext.lower() fname = ascii_filename(fname) show_download_info(result.title, parent=self) self.gui.download_ebook(result.downloads[ext], filename=fname, create_browser=result.create_browser) def open_store(self, result): self.gui.istores[result.store_name].open( self, result.detail_item, self.open_external.isChecked()) def check_progress(self): m = self.results_view.model() if not self.search_pool.threads_running( ) and not m.cover_pool.threads_running( ) and not m.details_pool.threads_running(): self.pi.stopAnimation() self.search.setText(self.SEARCH_TEXT) self.searching = False else: self.searching = True if unicode_type(self.search.text()) != self.STOP_TEXT: self.search.setText(self.STOP_TEXT) if not self.pi.isAnimated(): self.pi.startAnimation() def stores_select_all(self): for check in self.store_checks.values(): check.setChecked(True) def stores_select_invert(self): for check in self.store_checks.values(): check.setChecked(not check.isChecked()) def stores_select_none(self): for check in self.store_checks.values(): check.setChecked(False) def dialog_closed(self, result): self.results_view.model().closing() self.search_pool.abort() self.cache_pool.abort() self.save_state() def exec_(self): if unicode_type(self.search_edit.text()).strip() or unicode_type( self.search_title.text()).strip() or unicode_type( self.search_author.text()).strip(): self.do_search() return QDialog.exec_(self)
class BlockingJob(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) l = QVBoxLayout() self.setLayout(l) l.addStretch(10) self.pi = ProgressIndicator(self, 128) l.addWidget(self.pi, alignment=Qt.AlignHCenter) self.dummy = QLabel('<h2>\xa0') l.addSpacing(10) l.addWidget(self.dummy, alignment=Qt.AlignHCenter) l.addStretch(10) self.setVisible(False) self.text = '' def start(self): self.setGeometry(0, 0, self.parent().width(), self.parent().height()) self.setVisible(True) # Prevent any actions from being triggerred by key presses self.parent().setEnabled(False) self.raise_() self.setFocus(Qt.OtherFocusReason) self.pi.startAnimation() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) def stop(self): QApplication.restoreOverrideCursor() self.pi.stopAnimation() self.setVisible(False) self.parent().setEnabled(True) def job_done(self, callback, job): del job.callback self.stop() callback(job) def paintEvent(self, ev): br = ev.region().boundingRect() p = QPainter(self) p.setOpacity(0.2) p.fillRect(br, QBrush(self.palette().text())) p.end() QWidget.paintEvent(self, ev) p = QPainter(self) p.setClipRect(br) f = p.font() f.setBold(True) f.setPointSize(20) p.setFont(f) p.setPen(Qt.SolidLine) r = QRect(0, self.dummy.geometry().top() + 10, self.geometry().width(), 150) p.drawText(r, Qt.AlignHCenter | Qt.AlignTop | Qt.TextSingleLine, self.text) p.end() def eventFilter(self, obj, ev): if ev.type() in (ev.KeyPress, ev.KeyRelease): return True return QWidget.eventFilter(self, obj, ev) def set_msg(self, text): self.text = text def __call__(self, name, user_text, callback, function, *args, **kwargs): ' Run a job that blocks the GUI providing some feedback to the user ' self.set_msg(user_text) job = LongJob(name, user_text, Dispatcher(partial(self.job_done, callback)), function, *args, **kwargs) job.start() self.start()