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(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): # {{{ 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 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): # {{{ 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 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.CursorShape.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.AlignmentFlag.AlignCenter) self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignmentFlag.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.DialogCode.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.SortOrder.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.StandardButton.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 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)