def __call__(self, item, where): self.current_item, self.current_where = item, where self.current_name = None self.current_frag = None self.name.setText(_('(Untitled)')) dest_index, frag = 0, None if item is not None: if where is None: self.name.setText(item.data(0, Qt.DisplayRole) or '') self.name.setCursorPosition(0) toc = item.data(0, Qt.UserRole) if toc.dest: for i in xrange(self.dest_list.count()): litem = self.dest_list.item(i) if unicode(litem.data(Qt.DisplayRole) or '') == toc.dest: dest_index = i frag = toc.frag break self.dest_list.blockSignals(True) self.dest_list.setCurrentRow(dest_index) self.dest_list.blockSignals(False) item = self.dest_list.item(dest_index) self.current_changed(item) if frag: self.current_frag = frag QTimer.singleShot(1, self.show_frag)
def do_one_isbn_add(self): try: db = self.gui.library_view.model().db try: x = self.isbn_books.pop(0) except IndexError: self.gui.library_view.model().books_added(self.isbn_add_dialog.value) self.isbn_add_dialog.accept() self.gui.iactions['Edit Metadata'].download_metadata( ids=self.add_by_isbn_ids, ensure_fields=frozenset(['title', 'authors'])) return mi = MetaInformation(None) mi.isbn = x['isbn'] if self.isbn_add_tags: mi.tags = list(self.isbn_add_tags) fmts = [] if x['path'] is None else [x['path']] self.add_by_isbn_ids.add(db.import_book(mi, fmts)) self.isbn_add_dialog.value += 1 QTimer.singleShot(10, self.do_one_isbn_add) except: self.isbn_add_dialog.accept() raise
def check(self): if self.rejected: return if self.thread.is_alive(): QTimer.singleShot(100, self.check) else: self.accept()
def close_tab(self, tab=None): tab = tab or self.current_tab if tab is not None: self.delete_removed_tabs(self.tab_tree.remove_tab(tab)) if not self.tabs: self.open_url(WELCOME_URL, switch_to_tab=True) QTimer.singleShot(0, self.current_tab_changed)
def drop_event(self, event, mime_data): mime = 'application/calibre+from_library' if mime_data.hasFormat(mime): self.dropped_ids = tuple(map(int, str(mime_data.data(mime)).split())) QTimer.singleShot(1, self.do_drop) return True return False
def dump(self, items, out_stream, pdf_metadata): self.metadata = pdf_metadata self._delete_tmpdir() self.outline = Outline(self.toc, items) self.render_queue = items self.combine_queue = [] self.out_stream = out_stream self.insert_cover() self.render_succeeded = False self.current_page_num = self.doc.page_count() self.combine_queue.append(os.path.join(self.tmp_path, 'qprinter_out.pdf')) self.first_page = True self.setup_printer(self.combine_queue[-1]) QTimer.singleShot(0, self._render_book) self.loop.exec_() if self.painter is not None: self.painter.end() if self.printer is not None: self.printer.abort() if not self.render_succeeded: raise Exception('Rendering HTML to PDF failed')
def do_one_block(self): try: start_cursor, end_cursor = self.requests[0] except IndexError: return self.ignore_requests = True try: block = start_cursor.block() if not block.isValid(): self.requests.popleft() return formats, force_next_highlight = self.parse_single_block(block) self.apply_format_changes(block, formats) try: self.doc.markContentsDirty(block.position(), block.length()) except AttributeError: self.requests.clear() return ok = start_cursor.movePosition(start_cursor.NextBlock) if not ok: self.requests.popleft() return next_block = start_cursor.block() if next_block.position() > end_cursor.position(): if force_next_highlight: end_cursor.setPosition(next_block.position() + 1) else: self.requests.popleft() return finally: self.ignore_requests = False QTimer.singleShot(0, self.do_one_block)
def __init__(self, parent, library_path, wait_time=2): QDialog.__init__(self, parent) self.l = QVBoxLayout() self.setLayout(self.l) self.l1 = QLabel('<b>'+_('Restoring database from backups, do not' ' interrupt, this will happen in three stages')+'...') self.setWindowTitle(_('Restoring database')) self.l.addWidget(self.l1) self.pb = QProgressBar(self) self.l.addWidget(self.pb) self.pb.setMaximum(0) self.pb.setMinimum(0) self.msg = QLabel('') self.l.addWidget(self.msg) self.msg.setWordWrap(True) self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) self.l.addWidget(self.bb) self.bb.rejected.connect(self.reject) self.resize(self.sizeHint() + QSize(100, 50)) self.error = None self.rejected = False self.library_path = library_path self.update_signal.connect(self.do_update, type=Qt.QueuedConnection) from calibre.db.restore import Restore self.restorer = Restore(library_path, self) self.restorer.daemon = True # Give the metadata backup thread time to stop QTimer.singleShot(wait_time * 1000, self.start)
def stop_server(self): self.gui.content_server.threaded_exit() self.stopping_msg = info_dialog(self, _('Stopping'), _('Stopping server, this could take upto a minute, please wait...'), show_copy_button=False) QTimer.singleShot(500, self.check_exited) self.stopping_msg.exec_()
def __init__(self, container, do_embed=False): self.container = container self.log = self.logger = container.log self.do_embed = do_embed must_use_qt() self.parser = CSSParser(loglevel=logging.CRITICAL, log=logging.getLogger('calibre.css')) self.first_letter_pat = regex.compile(r'^[\p{Ps}\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]+', regex.VERSION1 | regex.UNICODE) self.loop = QEventLoop() self.view = QWebView() self.page = Page(self.log) self.view.setPage(self.page) self.page.setViewportSize(QSize(1200, 1600)) self.view.loadFinished.connect(self.collect, type=Qt.QueuedConnection) self.render_queue = list(container.spine_items) self.font_stats = {} self.font_usage_map = {} self.font_spec_map = {} self.font_rule_map = {} self.all_font_rules = {} QTimer.singleShot(0, self.render_book) if self.loop.exec_() == 1: raise Exception('Failed to gather statistics from book, see log for details')
def __init__(self, parent, db): QObject.__init__(self, parent) self.internet_connection_failed = False self._parent = parent self.no_internet_msg = _('Cannot download news as no internet connection ' 'is active') self.no_internet_dialog = d = error_dialog(self._parent, self.no_internet_msg, _('No internet connection'), show_copy_button=False) d.setModal(False) self.recipe_model = RecipeModel() self.db = db self.lock = QMutex(QMutex.Recursive) self.download_queue = set([]) self.news_menu = QMenu() self.news_icon = QIcon(I('news.png')) self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self) self.news_menu.addAction(self.scheduler_action) self.scheduler_action.triggered[bool].connect(self.show_dialog) self.cac = QAction(QIcon(I('user_profile.png')), _('Add a custom news source'), self) self.cac.triggered[bool].connect(self.customize_feeds) self.news_menu.addAction(self.cac) self.news_menu.addSeparator() self.all_action = self.news_menu.addAction( _('Download all scheduled news sources'), self.download_all_scheduled) self.timer = QTimer(self) self.timer.start(int(self.INTERVAL * 60 * 1000)) self.timer.timeout.connect(self.check) self.oldest = gconf['oldest_news'] QTimer.singleShot(5 * 1000, self.oldest_check)
def perform_action(self, ac, loc): if ac in ('new', 'existing'): self.callback(loc, copy_structure=self.copy_structure.isChecked()) else: # move library self.db.prefs.disable_setting = True abort_move = Event() pd = ProgressDialog(_('Moving library, please wait...'), _('Scanning...'), max=0, min=0, icon='lt.png', parent=self) pd.canceled_signal.connect(abort_move.set) self.parent().library_view.model().stop_metadata_backup() move_error = [] def do_move(): try: self.db.new_api.move_library_to(loc, abort=abort_move, progress=pd.show_new_progress) except Exception: import traceback move_error.append(traceback.format_exc()) finally: pd.finished_moving.emit() t = Thread(name='MoveLibrary', target=do_move) QTimer.singleShot(0, t.start) pd.exec_() if abort_move.is_set(): self.callback(self.db.library_path) return if move_error: error_dialog(self.parent(), _('Failed to move library'), _( 'There was an error while moving the library. The operation has been aborted. Click' ' "Show details" for details.'), det_msg=move_error[0], show=True) self.callback(self.db.library_path) return self.callback(loc, library_renamed=True)
def do_book_action(self): if self.wasCanceled(): return self.do_close() if self.i >= self.total_count: return self.do_close() book = self.indices[self.i] self.i += 1 # Get the title and build the caption and label text from the string parameters provided if self.db_type == 'calibre': dtitle = book[0].title elif self.db_type == 'kobo': dtitle = book.title self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count, self.status_msg_type, len(self.failures), self.action_type[1])) self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle)) # If a calibre db, feed the calibre bookmap to action.py's add_new_books method if self.db_type == 'calibre': if self.callback_fn([book]): self.successes.append(book) else: self.failures.append(book) # If a kobo db, feed the index to the kobo book to action.py's get_decrypted_kobo_books method elif self.db_type == 'kobo': if self.callback_fn(book): debug_print("DecryptAddProgressDialog::do_book_action - decrypted book: '%s'" % dtitle) self.successes.append(book) else: debug_print("DecryptAddProgressDialog::do_book_action - book decryption failed: '%s'" % dtitle) self.failures.append(book) self.setValue(self.i) # Lather, rinse, repeat. QTimer.singleShot(0, self.do_book_action)
def execute(self): if not self.timer.isActive() or self.is_date_changed: QTimer.singleShot(500, self.setTime) self.timer.stop() index = self.ui.countryList.currentIndex() ctx.installData.timezone = self.ui.countryList.itemData(index) ctx.logger.debug("Time zone selected as %s " % ctx.installData.timezone) if ctx.flags.install_type == ctx.STEP_BASE: #FIXME:Refactor hacky code ctx.installData.rootPassword = ctx.consts.default_password ctx.installData.hostName = yali.util.product_release() if ctx.storageInitialized: disks = filter(lambda d: not d.format.hidden, ctx.storage.disks) if len(disks) == 1: ctx.storage.clearPartDisks = [disks[0].name] ctx.mainScreen.step_increment = 2 else: ctx.mainScreen.step_increment = 1 return True else: self.pds_messagebox.setMessage(_("Storage Devices initialising...")) self.pds_messagebox.animate(start=MIDCENTER, stop=MIDCENTER) ctx.mainScreen.step_increment = 0 self.pthread.start() QTimer.singleShot(2, self.startStorageInitialize) return False return True
def do_book_action(self): if self.wasCanceled(): return self.do_close() if self.i >= self.total_count: return self.do_close() epub_format = self.entries[self.i] self.i += 1 # assign the elements of the 3-tuple details to legible variables book_id, mi, path = epub_format[0], epub_format[1], epub_format[2] # Get the title and build the caption and label text from the string parameters provided dtitle = mi.title self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count, self.status_msg_type, len(self.failures), self.action_type[1])) self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle)) # Send the necessary elements to the process_epub_formats callback function (action.py) # and record the results if self.callback_fn(book_id, mi, path): self.successes.append((book_id, mi, path)) else: self.failures.append((book_id, mi, path)) self.setValue(self.i) # Lather, rinse, repeat QTimer.singleShot(0, self.do_book_action)
def link_clicked(self, url): path = os.path.abspath(unicode(url.toLocalFile())) frag = None if path in self.iterator.spine: self.update_page_number() # Ensure page number is accurate as it is used for history self.history.add(self.pos.value()) path = self.iterator.spine[self.iterator.spine.index(path)] if url.hasFragment(): frag = unicode(url.fragment()) if path != self.current_page: self.pending_anchor = frag self.load_path(path) else: oldpos = self.view.document.ypos if frag: self.view.scroll_to(frag) else: # Scroll to top self.view.scroll_to(0) if self.view.document.ypos == oldpos: # If we are coming from goto_next_section() call this will # cause another goto next section call with the next toc # entry, since this one did not cause any scrolling at all. QTimer.singleShot(10, self.update_indexing_state) else: open_url(url)
def load_finished(self, ok): self.close_progress_indicator() path = self.view.path() try: index = self.iterator.spine.index(path) except (ValueError, AttributeError): return -1 self.current_page = self.iterator.spine[index] self.current_index = index self.set_page_number(self.view.scroll_fraction) QTimer.singleShot(100, self.update_indexing_state) if self.pending_search is not None: self.do_search(self.pending_search, self.pending_search_dir=='backwards') self.pending_search = None self.pending_search_dir = None if self.pending_anchor is not None: self.view.scroll_to(self.pending_anchor) self.pending_anchor = None if self.pending_reference is not None: self.view.goto(self.pending_reference) self.pending_reference = None if self.pending_bookmark is not None: self.goto_bookmark(self.pending_bookmark) self.pending_bookmark = None if self.pending_restore: self.view.document.page_position.restore() return self.current_index
def finalize_layout(self): self.status_bar.initialize(self.system_tray_icon) self.book_details.show_book_info.connect(self.iactions['Show Book Details'].show_book_info) self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book) self.book_details.cover_changed.connect(self.bd_cover_changed, type=Qt.QueuedConnection) self.book_details.cover_removed.connect(self.bd_cover_removed, type=Qt.QueuedConnection) self.book_details.remote_file_dropped.connect( self.iactions['Add Books'].remote_file_dropped_on_book, type=Qt.QueuedConnection) self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id) self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id) self.book_details.search_requested.connect(self.search.set_search_string) self.book_details.remove_specific_format.connect( self.iactions['Remove Books'].remove_format_by_id) self.book_details.save_specific_format.connect( self.iactions['Save To Disk'].save_library_format_by_ids) self.book_details.restore_specific_format.connect( self.iactions['Remove Books'].restore_format) self.book_details.copy_link.connect(self.bd_copy_link, type=Qt.QueuedConnection) self.book_details.view_device_book.connect( self.iactions['View'].view_device_book) self.book_details.manage_author.connect(lambda author:self.do_author_sort_edit(self, author, select_sort=False, select_link=False)) self.book_details.compare_specific_format.connect(self.compare_format) m = self.library_view.model() if m.rowCount(None) > 0: QTimer.singleShot(0, self.library_view.set_current_row) m.current_changed(self.library_view.currentIndex(), self.library_view.currentIndex()) self.library_view.setFocus(Qt.OtherFocusReason)
def __init__(self, log, parent=None): QDialog.__init__(self, parent) self.log = log self.l = l = QVBoxLayout() self.setLayout(l) self.tb = QTextBrowser(self) l.addWidget(self.tb) self.bb = QDialogButtonBox(QDialogButtonBox.Close) l.addWidget(self.bb) self.copy_button = self.bb.addButton(_('Copy to clipboard'), self.bb.ActionRole) self.copy_button.clicked.connect(self.copy_to_clipboard) self.copy_button.setIcon(QIcon(I('edit-copy.png'))) self.bb.rejected.connect(self.reject) self.bb.accepted.connect(self.accept) self.setWindowTitle(_('Download log')) self.setWindowIcon(QIcon(I('debug.png'))) self.resize(QSize(800, 400)) self.keep_updating = True self.last_html = None self.finished.connect(self.stop) QTimer.singleShot(100, self.update_log) self.show()
def confirm_delete(self, spine_items, other_names): spine_names = {name for name, remove in spine_items if remove} if not question_dialog(self, _('Are you sure?'), _( 'Are you sure you want to delete the selected files?'), det_msg='\n'.join(spine_names | other_names)): return self.delete_requested.emit(spine_items, other_names) QTimer.singleShot(10, self.refresh)
def add_event(self, event_type, msg, timer=False, host=None): """ Add event to events list :param event_type: the type of event: OK, DOWN, ACK, ... :type event_type: str :param msg: message of event :type msg: str :param timer: timer to hide event at end of time :type timer: bool :param host: data of a host to set ``Qt.UserRole`` :type host: None | str """ if not self.event_exist(msg): logger.debug( 'Add Event: msg: %s, timer: %s, host: %s', msg, timer, host ) event = EventItem() event.initialize(event_type, msg, timer=timer, host=host) self.events_list.insertItem(0, event) if timer: event_duration = int( settings.get_config('Alignak-app', 'notification_duration') ) * 1000 QTimer.singleShot( event_duration, lambda: self.remove_timer_event(event) ) else: logger.debug( 'Event with msg: %s already exist.', msg )
def main(): from calibre.gui2 import Application from PyQt5.Qt import QMainWindow, QStatusBar, QTimer app = Application([]) w = QMainWindow() s = QStatusBar(w) w.setStatusBar(s) s.showMessage("Testing ProceedQuestion") w.show() p = ProceedQuestion(w) def doit(): p.dummy_question() p.dummy_question(action_label="A very long button for testing relayout (indeed)") p( lambda p: None, None, "ass2", "ass2", "testing2", "testing2", det_msg="details shown first, with a long line to test wrapping of text and width layout", show_det=True, show_ok=True, ) QTimer.singleShot(10, doit) app.exec_()
class DetailView(QDialog, Ui_Dialog): # {{{ def __init__(self, parent, job): QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle(job.description) self.job = job self.html_view = (hasattr(job, 'html_details') and not getattr(job, 'ignore_html_details', False)) if self.html_view: self.log.setVisible(False) else: self.tb.setVisible(False) self.next_pos = 0 self.update() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(1000) v = self.log.verticalScrollBar() v.setValue(v.maximum()) def update(self): if self.html_view: html = self.job.html_details if len(html) > self.next_pos: self.next_pos = len(html) self.tb.setHtml( '<pre style="font-family:monospace">%s</pre>'%html) else: f = self.job.log_file f.seek(self.next_pos) more = f.read() self.next_pos = f.tell() if more: self.log.appendPlainText(more.decode('utf-8', 'replace'))
def update_log(self): if not self.keep_updating: return html = self.log.html if html != self.last_html: self.last_html = html self.tb.setHtml('<pre style="font-family:monospace">%s</pre>'%html) QTimer.singleShot(1000, self.update_log)
def __init__(self, parent, request): QNetworkReply.__init__(self, parent) self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered) self.setHeader(QNetworkRequest.ContentTypeHeader, 'application/octet-stream') self.setHeader(QNetworkRequest.ContentLengthHeader, 0) self.setRequest(request) self.setUrl(request.url()) QTimer.singleShot(0, self.finalize_reply)
def check_exited(self): if getattr(self.server, 'is_running', False): QTimer.singleShot(20, self.check_exited) return self.gui.content_server = None self.main_tab.update_button_state() self.stopping_msg.accept()
def check_exited(self): if self.gui.content_server.is_running: QTimer.singleShot(20, self.check_exited) if not self.stopping_msg.isVisible(): self.stopping_msg.exec_() return self.gui.content_server = None self.stopping_msg.accept()
class ServerDialog(QDialog, Ui_ServerDialog): def startServerClicked(self): self.message("ServerDialog.startServerClicked - Start / threadId="+str(int(QThread.currentThreadId()))) self.worker.startServer(); def stopServerClicked(self): self.message("ServerDialog.stopServerClicked - Start / threadId="+str(int(QThread.currentThreadId()))) self.worker.stopServer(); @pyqtSlot('bool') def setConnectionStatus(self, enabled): if not enabled: self.startServerButton.clicked.disconnect(self.stopServerClicked) self.startServerButton.clicked.connect(self.startServerClicked) self.startServerButton.setText("Start server") else: self.startServerButton.clicked.disconnect(self.startServerClicked) self.startServerButton.clicked.connect(self.stopServerClicked) self.startServerButton.setText("Stop server") def message(self, str_): self.messageQueue.put(str_) def update(self): scrollToBottom = False; while self.messageQueue.empty()!=True: message = self.messageQueue.get() listItem = QListWidgetItem() listItem.setText(message) self.messageList.addItem(listItem) scrollToBottom = True; if scrollToBottom: self.messageList.scrollToBottom() def __init__(self): super().__init__() self.messageQueue = Queue() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(250) self.workerThread = WorkThread() self.worker=Server(self.message) self.worker.isConnected.connect(self.setConnectionStatus) self.worker.moveToThread(self.workerThread) self.workerThread.start(QThread.HighPriority) self.setupUi(self) self.startServerButton.clicked.connect(self.startServerClicked) def closeEvent(self, event): self.worker.stopServer() # TODO: WAIT HERE UNTIL ARE THREADS ARE DEAD self.accept() super().closeEvent(event)
def __init__(self, parent=None, initial=None): QDialog.__init__(self, parent) self.setWindowTitle(_('Choose a texture')) self.l = l = QVBoxLayout() self.setLayout(l) self.tdir = texture_dir() self.images = il = QListWidget(self) il.itemDoubleClicked.connect(self.accept, type=Qt.QueuedConnection) il.setIconSize(QSize(256, 256)) il.setViewMode(il.IconMode) il.setFlow(il.LeftToRight) il.setSpacing(20) il.setSelectionMode(il.SingleSelection) il.itemSelectionChanged.connect(self.update_remove_state) l.addWidget(il) self.ad = ad = QLabel(_('The builtin textures come from <a href="https://subtlepatterns.com">subtlepatterns.com</a>.')) ad.setOpenExternalLinks(True) ad.setWordWrap(True) l.addWidget(ad) self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) b = self.add_button = bb.addButton(_('Add texture'), bb.ActionRole) b.setIcon(QIcon(I('plus.png'))) b.clicked.connect(self.add_texture) b = self.remove_button = bb.addButton(_('Remove texture'), bb.ActionRole) b.setIcon(QIcon(I('minus.png'))) b.clicked.connect(self.remove_texture) l.addWidget(bb) images = [{ 'fname': ':'+os.path.basename(x), 'path': x, 'name': ' '.join(map(lambda s: s.capitalize(), os.path.splitext(os.path.basename(x))[0].split('_'))) } for x in glob.glob(I('textures/*.png'))] + [{ 'fname': os.path.basename(x), 'path': x, 'name': os.path.splitext(os.path.basename(x))[0], } for x in glob.glob(os.path.join(self.tdir, '*')) if x.rpartition('.')[-1].lower() in {'jpeg', 'png', 'jpg'}] images.sort(key=lambda x:sort_key(x['name'])) for i in images: self.create_item(i) self.update_remove_state() if initial: existing = {unicode_type(i.data(Qt.UserRole) or ''):i for i in (self.images.item(c) for c in range(self.images.count()))} item = existing.get(initial, None) if item is not None: item.setSelected(True) QTimer.singleShot(100, partial(il.scrollToItem, item)) self.resize(QSize(950, 650))
def cover_browser_shown(self): self.cover_flow.setFocus(Qt.OtherFocusReason) if CoverFlow is not None: self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row()) self.cover_flow_syncing_enabled = True QTimer.singleShot(500, self.cover_flow_do_sync) self.library_view.setCurrentIndex( self.library_view.currentIndex()) self.library_view.scroll_to_row(self.library_view.currentIndex().row())
class SearchBox2(QComboBox): # {{{ ''' To use this class: * Call initialize() * Connect to the search() and cleared() signals from this widget. * Connect to the changed() signal to know when the box content changes * Connect to focus_to_library() signal to be told to manually change focus * Call search_done() after every search is complete * Call set_search_string() to perform a search programmatically * You can use the current_text property to get the current search text Be aware that if you are using it in a slot connected to the changed() signal, if the connection is not queued it will not be accurate. ''' INTERVAL = 1500 #: Time to wait before emitting search signal MAX_COUNT = 25 search = pyqtSignal(object) cleared = pyqtSignal() changed = pyqtSignal() focus_to_library = pyqtSignal() def __init__(self, parent=None): QComboBox.__init__(self, parent) self.normal_background = 'rgb(255, 255, 255, 0%)' self.line_edit = SearchLineEdit(self) self.setLineEdit(self.line_edit) c = self.line_edit.completer() c.setCompletionMode(c.PopupCompletion) c.highlighted[str].connect(self.completer_used) self.line_edit.key_pressed.connect(self.key_pressed, type=Qt.DirectConnection) # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807 self.activated[str].connect(self.history_selected, type=Qt.QueuedConnection) self.setEditable(True) self.as_you_type = True self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection) self.setInsertPolicy(self.NoInsert) self.setMaxCount(self.MAX_COUNT) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setMinimumContentsLength(25) self._in_a_search = False self.tool_tip_text = self.toolTip() def initialize(self, opt_name, colorize=False, help_text=_('Search')): self.as_you_type = config['search_as_you_type'] self.opt_name = opt_name items = [] for item in config[opt_name]: if item not in items: items.append(item) self.addItems(items) self.line_edit.setPlaceholderText(help_text) self.colorize = colorize self.clear() def hide_completer_popup(self): try: self.lineEdit().completer().popup().setVisible(False) except: pass def normalize_state(self): self.setToolTip(self.tool_tip_text) self.line_edit.setStyleSheet( 'QLineEdit{color:none;background-color:%s;}' % self.normal_background) def text(self): return self.currentText() def clear_history(self, *args): QComboBox.clear(self) def clear(self, emit_search=True): self.normalize_state() self.setEditText('') if emit_search: self.search.emit('') self._in_a_search = False self.cleared.emit() def clear_clicked(self, *args): self.clear() def search_done(self, ok): if isinstance(ok, basestring): self.setToolTip(ok) ok = False if not unicode(self.currentText()).strip(): self.clear(emit_search=False) return self._in_a_search = ok col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)' if not self.colorize: col = self.normal_background self.line_edit.setStyleSheet( 'QLineEdit{color:black;background-color:%s;}' % col) # Comes from the lineEdit control def key_pressed(self, event): k = event.key() if k in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End, Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_unknown): return self.normalize_state() if self._in_a_search: self.changed.emit() self._in_a_search = False if event.key() in (Qt.Key_Return, Qt.Key_Enter): self.do_search() self.focus_to_library.emit() elif self.as_you_type and unicode(event.text()): self.timer.start(1500) # Comes from the combobox itself def keyPressEvent(self, event): k = event.key() if k in (Qt.Key_Enter, Qt.Key_Return): return self.do_search() if k not in (Qt.Key_Up, Qt.Key_Down): QComboBox.keyPressEvent(self, event) else: self.blockSignals(True) self.normalize_state() QComboBox.keyPressEvent(self, event) self.blockSignals(False) def completer_used(self, text): self.timer.stop() self.normalize_state() def timer_event(self): self._do_search(as_you_type=True) def history_selected(self, text): self.changed.emit() self.do_search() def _do_search(self, store_in_history=True, as_you_type=False): self.hide_completer_popup() text = unicode(self.currentText()).strip() if not text: return self.clear() if as_you_type: text = AsYouType(text) self.search.emit(text) if store_in_history: idx = self.findText(text, Qt.MatchFixedString | Qt.MatchCaseSensitive) self.block_signals(True) if idx < 0: self.insertItem(0, text) else: t = self.itemText(idx) self.removeItem(idx) self.insertItem(0, t) self.setCurrentIndex(0) self.block_signals(False) history = [unicode(self.itemText(i)) for i in range(self.count())] config[self.opt_name] = history def do_search(self, *args): self._do_search() def block_signals(self, yes): self.blockSignals(yes) self.line_edit.blockSignals(yes) def set_search_string(self, txt, store_in_history=False, emit_changed=True): if not store_in_history: self.activated[str].disconnect() try: self.setFocus(Qt.OtherFocusReason) if not txt: self.clear() else: self.normalize_state() # must turn on case sensitivity here so that tag browser strings # are not case-insensitively replaced from history self.line_edit.completer().setCaseSensitivity(Qt.CaseSensitive) self.setEditText(txt) self.line_edit.end(False) if emit_changed: self.changed.emit() self._do_search(store_in_history=store_in_history) self.line_edit.completer().setCaseSensitivity( Qt.CaseInsensitive) self.focus_to_library.emit() finally: if not store_in_history: # QueuedConnection as workaround for https://bugreports.qt-project.org/browse/QTBUG-40807 self.activated[str].connect(self.history_selected, type=Qt.QueuedConnection) def search_as_you_type(self, enabled): self.as_you_type = enabled def in_a_search(self): return self._in_a_search @property def current_text(self): return unicode(self.lineEdit().text())
def setEditorData(self, editor, index): name = unicode(index.data(NAME_ROLE) or '') # We do this because Qt calls selectAll() unconditionally on the # editor, and we want only a part of the file name to be selected QTimer.singleShot(0, partial(self.set_editor_data, name, editor))
def __init__(self, parent): QWidget.__init__(self, parent) self.parent = parent self._layout = QVBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0, 0, 0, 0) # Set up the find box & button search_layout = QHBoxLayout() self._layout.addLayout(search_layout) self.item_search = HistoryLineEdit(parent) self.item_search.setMinimumContentsLength(5) self.item_search.setSizeAdjustPolicy( self.item_search.AdjustToMinimumContentsLengthWithIcon) try: self.item_search.lineEdit().setPlaceholderText( _('Find item in tag browser')) except: pass # Using Qt < 4.7 self.item_search.setToolTip( _('Search for items. This is a "contains" search; items containing the\n' 'text anywhere in the name will be found. You can limit the search\n' 'to particular categories using syntax similar to search. For example,\n' 'tags:foo will find foo in any tag, but not in authors etc. Entering\n' '*foo will filter all categories at once, showing only those items\n' 'containing the text "foo"')) search_layout.addWidget(self.item_search) # Not sure if the shortcut should be translatable ... sc = QShortcut(QKeySequence(_('ALT+f')), parent) sc.activated.connect(self.set_focus_to_find_box) self.search_button = QToolButton() self.search_button.setText(_('F&ind')) self.search_button.setToolTip(_('Find the first/next matching item')) search_layout.addWidget(self.search_button) self.expand_button = QToolButton() self.expand_button.setText('-') self.expand_button.setToolTip(_('Collapse all categories')) search_layout.addWidget(self.expand_button) search_layout.setStretch(0, 10) search_layout.setStretch(1, 1) search_layout.setStretch(2, 1) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.initialize('tag_browser_search') self.item_search.lineEdit().returnPressed.connect(self.do_find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive) parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self.expand_button.clicked.connect(self.tags_view.collapseAll) self._layout.addWidget(parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText( '<p><b>' + _('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10, 20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) parent.alter_tb = l = QPushButton(parent) l.setText(_('Alter Tag Browser')) l.setIcon(QIcon(I('tags.png'))) l.m = QMenu() l.setMenu(l.m) self._layout.addWidget(l) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Sort by name'), _('Sort by number of books'), _('Sort by average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip(_('Set the sort order for entries in the Tag Browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate( (_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag Browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip( _('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m)
def show_frag(self): self.view.show_frag(self.current_frag) QTimer.singleShot(1, self.check_frag)
def test(editor): c = editor.__c = CompletionPopup(editor.editor, max_height=100) c.set_items('one two three four five six seven eight nine ten'.split()) QTimer.singleShot(10, c.show)
class TagBrowserWidget(QFrame): # {{{ def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle( QFrame.Shape.NoFrame if gprefs['tag_browser_old_look'] else QFrame. Shape.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) tbb.clear_find.connect(self.reset_find) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.Shape.StyledPanel) l.setAutoFillBackground(True) l.setText( '<p><b>' + _('No more matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignmentFlag.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10, 20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.ConnectionType.QueuedConnection) # The Alter Tag Browser button l = self.alter_tb self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate( (_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip(_('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate( (_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip( _('All of these category_managers are available by right-clicking ' 'on items in the Tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut( 'tag browser set focus', _("Give the Tag browser keyboard focus"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.give_tb_focus) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def save_state(self): gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked()) def toggle_item(self): self.tags_view.toggle_current_index() def give_tb_focus(self, *args): if gprefs['tag_browser_allow_keyboard_focus']: tb = self.tags_view if tb.hasFocus(): self._parent.shift_esc() elif self._parent.current_view() == self._parent.library_view: tb.setFocus() idx = tb.currentIndex() if not idx.isValid(): idx = tb.model().createIndex(0, 0) tb.setCurrentIndex(idx) def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) if not to_what: self._parent.shift_esc() def find_text_changed(self, str_): self.current_find_position = None def set_focus_to_find_box(self): self.tb_bar.set_focus_to_find_box() def do_find(self, str_=None): self.current_find_position = None self.find() @property def find_text(self): return unicode_type(self.item_search.currentText()).strip() def reset_find(self): model = self.tags_view.model() model.clear_boxed() if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None def find(self): model = self.tags_view.model() model.clear_boxed() # When a key is specified don't use the auto-collapsing search. # A colon separates the lookup key from the search string. # A leading colon says not to use autocollapsing search but search all keys txt = self.find_text colon = txt.find(':') if colon >= 0: key = self._parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) if key in self._parent.library_view.model().db.field_metadata: txt = txt[colon + 1:] else: key = '' txt = txt[1:] if colon == 0 else txt else: key = None # key is None indicates that no colon was found. # key == '' means either a leading : was found or the key is invalid # At this point the txt might have a leading =, in which case do an # exact match search if (gprefs.get('tag_browser_always_autocollapse', False) and key is None and not txt.startswith('*')): txt = '*' + txt if txt.startswith('*'): self.tags_view.collapseAll() model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) if txt.startswith('='): equals_match = True txt = txt[1:] else: equals_match = False self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position, equals_match=equals_match) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def keyPressEvent(self, ev): if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and self.find_text: self.find() ev.accept() return return QFrame.keyPressEvent(self, ev)
def start(self): t = self.thread = Thread(target=self.vacuum) t.daemon = True t.start() QTimer.singleShot(100, self.check) self.exec_()
def cover_view_resized(self, event): QTimer.singleShot(1, self.resize_cover)
def update(self): if self.worker.is_alive(): QTimer.singleShot(50, self.update) else: self.process_results()
class CoverDelegate(QStyledItemDelegate): # {{{ needs_redraw = pyqtSignal() def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) self.angle = 0 self.timer = QTimer(self) self.timer.timeout.connect(self.frame_changed) self.color = parent.palette().color(QPalette.WindowText) self.spinner_width = 64 def frame_changed(self, *args): self.angle = (self.angle + 30) % 360 self.needs_redraw.emit() def start_animation(self): self.angle = 0 self.timer.start(200) def stop_animation(self): self.timer.stop() def draw_spinner(self, painter, rect): width = rect.width() outer_radius = (width - 1) * 0.5 inner_radius = (width - 1) * 0.5 * 0.38 capsule_height = outer_radius - inner_radius capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35)) capsule_radius = capsule_width // 2 painter.save() painter.setRenderHint(painter.Antialiasing) for i in xrange(12): color = QColor(self.color) color.setAlphaF(1.0 - (i / 12.0)) painter.setPen(Qt.NoPen) painter.setBrush(color) painter.save() painter.translate(rect.center()) painter.rotate(self.angle - i * 30.0) painter.drawRoundedRect(-capsule_width * 0.5, -(inner_radius + capsule_height), capsule_width, capsule_height, capsule_radius, capsule_radius) painter.restore() painter.restore() def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, index) style = QApplication.style() waiting = self.timer.isActive() and bool(index.data(Qt.UserRole)) if waiting: rect = QRect(0, 0, self.spinner_width, self.spinner_width) rect.moveCenter(option.rect.center()) self.draw_spinner(painter, rect) else: # Ensure the cover is rendered over any selection rect style.drawItemPixmap(painter, option.rect, Qt.AlignTop | Qt.AlignHCenter, QPixmap(index.data(Qt.DecorationRole)))
class TagListEditor(QDialog, Ui_TagListEditor): def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter, ttm_is_first_letter=False): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) self.search_box.setMinimumContentsLength(25) # Put the category name into the title bar t = self.windowTitle() self.category_name = cat_name self.setWindowTitle(t + ' (' + cat_name + ')') # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags() & (~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) # Get saved geometry info try: self.table_column_widths = \ gprefs.get('tag_list_editor_table_widths', None) except: pass # initialization self.to_rename = {} self.to_delete = set() self.all_tags = {} self.original_names = {} self.ordered_tags = [] self.sorter = sorter self.get_book_ids = get_book_ids self.text_before_editing = '' # Capture clicks on the horizontal header to sort the table columns hh = self.table.horizontalHeader() hh.sectionResized.connect(self.table_column_resized) hh.setSectionsClickable(True) hh.sectionClicked.connect(self.do_sort) hh.setSortIndicatorShown(True) self.last_sorted_by = 'name' self.name_order = 0 self.count_order = 1 self.was_order = 1 self.edit_delegate = EditColumnDelegate(self.table) self.edit_delegate.editing_finished.connect(self.stop_editing) self.edit_delegate.editing_started.connect(self.start_editing) self.table.setItemDelegateForColumn(0, self.edit_delegate) if prefs['use_primary_find_in_search']: self.string_contains = primary_contains else: self.string_contains = contains self.delete_button.clicked.connect(self.delete_tags) self.table.delete_pressed.connect(self.delete_pressed) self.rename_button.clicked.connect(self.rename_tag) self.undo_button.clicked.connect(self.undo_edit) self.table.itemDoubleClicked.connect(self._rename_tag) self.table.itemChanged.connect(self.finish_editing) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) self.search_box.initialize('tag_list_search_box_' + cat_name) le = self.search_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_search) self.search_box.textChanged.connect(self.search_text_changed) self.search_button.clicked.connect(self.do_search) self.search_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10, 0) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.filter_box.initialize('tag_list_filter_box_' + cat_name) le = self.filter_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_filter) le.returnPressed.connect(self.do_filter) self.filter_button.clicked.connect(self.do_filter) self.apply_vl_checkbox.clicked.connect(self.vl_box_changed) self.table.setEditTriggers(QTableWidget.EditKeyPressed) try: geom = gprefs.get('tag_list_editor_dialog_geometry', None) if geom is not None: QApplication.instance().safe_restore_geometry( self, QByteArray(geom)) else: self.resize(self.sizeHint() + QSize(150, 100)) except: pass # Add the data self.search_item_row = -1 self.fill_in_table(None, tag_to_match, ttm_is_first_letter) def vl_box_changed(self): self.search_item_row = -1 self.fill_in_table(None, None, False) def do_search(self): self.not_found_label.setVisible(False) find_text = icu_lower(unicode_type(self.search_box.currentText())) if not find_text: return for _ in range(0, self.table.rowCount()): r = self.search_item_row = (self.search_item_row + 1) % self.table.rowCount() if self.string_contains(find_text, self.table.item(r, 0).text()): self.table.setCurrentItem(self.table.item(r, 0)) self.table.setFocus(True) return # Nothing found. Pop up the little dialog for 1.5 seconds self.not_found_label.setVisible(True) self.not_found_label_timer.start(1500) def search_text_changed(self): self.search_item_row = -1 def clear_search(self): self.search_item_row = -1 self.search_box.setText('') def fill_in_table(self, tags, tag_to_match, ttm_is_first_letter): data = self.get_book_ids(self.apply_vl_checkbox.isChecked()) self.all_tags = {} filter_text = icu_lower(unicode_type(self.filter_box.text())) for k, v, count in data: if not filter_text or self.string_contains(filter_text, icu_lower(v)): self.all_tags[v] = { 'key': k, 'count': count, 'cur_name': v, 'is_deleted': k in self.to_delete } self.original_names[k] = v self.edit_delegate.set_completion_data(self.original_names.values()) self.ordered_tags = sorted(self.all_tags.keys(), key=self.sorter) if tags is None: tags = self.ordered_tags select_item = None self.table.blockSignals(True) self.table.clear() self.table.setColumnCount(3) self.name_col = QTableWidgetItem(self.category_name) self.table.setHorizontalHeaderItem(0, self.name_col) self.count_col = QTableWidgetItem(_('Count')) self.table.setHorizontalHeaderItem(1, self.count_col) self.was_col = QTableWidgetItem(_('Was')) self.table.setHorizontalHeaderItem(2, self.was_col) self.table.setRowCount(len(tags)) for row, tag in enumerate(tags): item = NameTableWidgetItem(self.sorter) item.set_is_deleted(self.all_tags[tag]['is_deleted']) _id = self.all_tags[tag]['key'] item.setData(Qt.UserRole, _id) item.set_initial_text(tag) if _id in self.to_rename: item.setText(self.to_rename[_id]) else: item.setText(tag) item.setFlags(item.flags() | Qt.ItemIsSelectable | Qt.ItemIsEditable) self.table.setItem(row, 0, item) if select_item is None: if ttm_is_first_letter: if primary_startswith(tag, tag_to_match): select_item = item elif tag == tag_to_match: select_item = item item = CountTableWidgetItem(self.all_tags[tag]['count']) # only the name column can be selected item.setFlags(item.flags() & ~(Qt.ItemIsSelectable | Qt.ItemIsEditable)) self.table.setItem(row, 1, item) item = QTableWidgetItem() item.setFlags(item.flags() & ~(Qt.ItemIsSelectable | Qt.ItemIsEditable)) if _id in self.to_rename or _id in self.to_delete: item.setData(Qt.DisplayRole, tag) self.table.setItem(row, 2, item) if self.last_sorted_by == 'name': self.table.sortByColumn(0, self.name_order) elif self.last_sorted_by == 'count': self.table.sortByColumn(1, self.count_order) else: self.table.sortByColumn(2, self.was_order) if select_item is not None: self.table.setCurrentItem(select_item) self.start_find_pos = select_item.row() else: self.table.setCurrentCell(0, 0) self.start_find_pos = -1 self.table.blockSignals(False) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def clear_filter(self): self.filter_box.setText('') self.fill_in_table(None, None, False) def do_filter(self): self.fill_in_table(None, None, False) def table_column_resized(self, col, old, new): self.table_column_widths = [] for c in range(0, self.table.columnCount()): self.table_column_widths.append(self.table.columnWidth(c)) def resizeEvent(self, *args): QDialog.resizeEvent(self, *args) if self.table_column_widths is not None: for c, w in enumerate(self.table_column_widths): self.table.setColumnWidth(c, w) else: # the vertical scroll bar might not be rendered, so might not yet # have a width. Assume 25. Not a problem because user-changed column # widths will be remembered w = self.table.width() - 25 - self.table.verticalHeader().width() w //= self.table.columnCount() for c in range(0, self.table.columnCount()): self.table.setColumnWidth(c, w) def save_geometry(self): gprefs['tag_list_editor_table_widths'] = self.table_column_widths gprefs['tag_list_editor_dialog_geometry'] = bytearray( self.saveGeometry()) def start_editing(self, on_row): items = self.table.selectedItems() self.table.blockSignals(True) for item in items: if item.row() != on_row: item.set_placeholder(_('Editing...')) else: self.text_before_editing = item.text() self.table.blockSignals(False) def stop_editing(self, on_row): items = self.table.selectedItems() self.table.blockSignals(True) for item in items: if item.row() != on_row and item.is_placeholder: item.reset_placeholder() self.table.blockSignals(False) def finish_editing(self, edited_item): if not edited_item.text(): error_dialog( self, _('Item is blank'), _('An item cannot be set to nothing. Delete it instead.'), show=True) self.table.blockSignals(True) edited_item.setText(self.text_before_editing) self.table.blockSignals(False) return items = self.table.selectedItems() self.table.blockSignals(True) for item in items: id_ = int(item.data(Qt.UserRole)) self.to_rename[id_] = unicode_type(edited_item.text()) orig = self.table.item(item.row(), 2) item.setText(edited_item.text()) orig.setData(Qt.DisplayRole, item.initial_text()) self.table.blockSignals(False) def undo_edit(self): indexes = self.table.selectionModel().selectedRows() if not indexes: error_dialog( self, _('No item selected'), _('You must select one item from the list of Available items.') ).exec_() return if not confirm(_('Do you really want to undo your changes?'), 'tag_list_editor_undo'): return self.table.blockSignals(True) for idx in indexes: row = idx.row() item = self.table.item(row, 0) item.setText(item.initial_text()) item.set_is_deleted(False) self.to_delete.discard(int(item.data(Qt.UserRole))) self.to_rename.pop(int(item.data(Qt.UserRole)), None) self.table.item(row, 2).setData(Qt.DisplayRole, '') self.table.blockSignals(False) def rename_tag(self): item = self.table.item(self.table.currentRow(), 0) self._rename_tag(item) def _rename_tag(self, item): if item is None: error_dialog( self, _('No item selected'), _('You must select one item from the list of Available items.') ).exec_() return col_zero_item = self.table.item(item.row(), 0) if col_zero_item.is_deleted: if not question_dialog( self, _('Undelete item?'), '<p>' + _('That item is deleted. Do you want to undelete it?') + '<br>'): return col_zero_item.set_is_deleted(False) self.to_delete.discard(int(col_zero_item.data(Qt.UserRole))) orig = self.table.item(col_zero_item.row(), 2) self.table.blockSignals(True) orig.setData(Qt.DisplayRole, '') self.table.blockSignals(False) else: self.table.editItem(item) def delete_pressed(self): if self.table.currentColumn() == 0: self.delete_tags() def delete_tags(self): deletes = self.table.selectedItems() if not deletes: error_dialog( self, _('No items selected'), _('You must select at least one item from the list.')).exec_() return to_del = [] for item in deletes: if not item.is_deleted: to_del.append(item) if to_del: ct = ', '.join([unicode_type(item.text()) for item in to_del]) if not confirm( '<p>' + _('Are you sure you want to delete the following items?') + '<br>' + ct, 'tag_list_editor_delete'): return row = self.table.row(deletes[0]) self.table.blockSignals(True) for item in deletes: id_ = int(item.data(Qt.UserRole)) self.to_delete.add(id_) item.set_is_deleted(True) orig = self.table.item(item.row(), 2) orig.setData(Qt.DisplayRole, item.initial_text()) self.table.blockSignals(False) if row >= self.table.rowCount(): row = self.table.rowCount() - 1 if row >= 0: self.table.scrollToItem(self.table.item(row, 0)) def do_sort(self, section): (self.do_sort_by_name, self.do_sort_by_count, self.do_sort_by_was)[section]() def do_sort_by_name(self): self.name_order = 1 - self.name_order self.last_sorted_by = 'name' self.table.sortByColumn(0, self.name_order) def do_sort_by_count(self): self.count_order = 1 - self.count_order self.last_sorted_by = 'count' self.table.sortByColumn(1, self.count_order) def do_sort_by_was(self): self.was_order = 1 - self.was_order self.last_sorted_by = 'count' self.table.sortByColumn(2, self.was_order) def accepted(self): self.save_geometry()
def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter, ttm_is_first_letter=False): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) self.search_box.setMinimumContentsLength(25) # Put the category name into the title bar t = self.windowTitle() self.category_name = cat_name self.setWindowTitle(t + ' (' + cat_name + ')') # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags() & (~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) # Get saved geometry info try: self.table_column_widths = \ gprefs.get('tag_list_editor_table_widths', None) except: pass # initialization self.to_rename = {} self.to_delete = set() self.all_tags = {} self.original_names = {} self.ordered_tags = [] self.sorter = sorter self.get_book_ids = get_book_ids self.text_before_editing = '' # Capture clicks on the horizontal header to sort the table columns hh = self.table.horizontalHeader() hh.sectionResized.connect(self.table_column_resized) hh.setSectionsClickable(True) hh.sectionClicked.connect(self.do_sort) hh.setSortIndicatorShown(True) self.last_sorted_by = 'name' self.name_order = 0 self.count_order = 1 self.was_order = 1 self.edit_delegate = EditColumnDelegate(self.table) self.edit_delegate.editing_finished.connect(self.stop_editing) self.edit_delegate.editing_started.connect(self.start_editing) self.table.setItemDelegateForColumn(0, self.edit_delegate) if prefs['use_primary_find_in_search']: self.string_contains = primary_contains else: self.string_contains = contains self.delete_button.clicked.connect(self.delete_tags) self.table.delete_pressed.connect(self.delete_pressed) self.rename_button.clicked.connect(self.rename_tag) self.undo_button.clicked.connect(self.undo_edit) self.table.itemDoubleClicked.connect(self._rename_tag) self.table.itemChanged.connect(self.finish_editing) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) self.search_box.initialize('tag_list_search_box_' + cat_name) le = self.search_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_search) self.search_box.textChanged.connect(self.search_text_changed) self.search_button.clicked.connect(self.do_search) self.search_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10, 0) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.filter_box.initialize('tag_list_filter_box_' + cat_name) le = self.filter_box.lineEdit() ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) if ac is not None: ac.triggered.connect(self.clear_filter) le.returnPressed.connect(self.do_filter) self.filter_button.clicked.connect(self.do_filter) self.apply_vl_checkbox.clicked.connect(self.vl_box_changed) self.table.setEditTriggers(QTableWidget.EditKeyPressed) try: geom = gprefs.get('tag_list_editor_dialog_geometry', None) if geom is not None: QApplication.instance().safe_restore_geometry( self, QByteArray(geom)) else: self.resize(self.sizeHint() + QSize(150, 100)) except: pass # Add the data self.search_item_row = -1 self.fill_in_table(None, tag_to_match, ttm_is_first_letter)
def eventFilter(self, obj, e): 'Redirect key presses from the popup to the widget' widget = self.parent() if widget is None or sip.isdeleted(widget): return False etype = e.type() if obj is not self: return QObject.eventFilter(self, obj, e) # self.debug_event(e) if etype == e.KeyPress: try: key = e.key() except AttributeError: return QObject.eventFilter(self, obj, e) if key == Qt.Key_Escape: self.hide() e.accept() return True if key == Qt.Key_F4 and e.modifiers() & Qt.AltModifier: self.hide() e.accept() return True if key in (Qt.Key_Enter, Qt.Key_Return): # We handle this explicitly because on OS X activated() is # not emitted on pressing Enter. idx = self.currentIndex() if idx.isValid(): self.item_chosen(idx) self.hide() e.accept() return True if key == Qt.Key_Tab: idx = self.currentIndex() if idx.isValid(): self.item_chosen(idx) self.hide() elif self.model().rowCount() > 0: self.next_match() e.accept() return True if key in (Qt.Key_PageUp, Qt.Key_PageDown): # Let the list view handle these keys return False if key in (Qt.Key_Up, Qt.Key_Down): self.next_match(previous=key == Qt.Key_Up) e.accept() return True # Send to widget widget.eat_focus_out = False widget.keyPressEvent(e) widget.eat_focus_out = True if not widget.hasFocus(): # Widget lost focus hide the popup self.hide() if e.isAccepted(): return True elif ismacos and etype == e.InputMethodQuery and e.queries() == (Qt.ImHints | Qt.ImEnabled) and self.isVisible(): # In Qt 5 the Esc key causes this event and the line edit does not # handle it, which causes the parent dialog to be closed # See https://bugreports.qt-project.org/browse/QTBUG-41806 e.accept() return True elif etype == e.MouseButtonPress and hasattr(e, 'globalPos') and not self.rect().contains(self.mapFromGlobal(e.globalPos())): # A click outside the popup, close it if isinstance(widget, QComboBox): # This workaround is needed to ensure clicking on the drop down # arrow of the combobox closes the popup opt = QStyleOptionComboBox() widget.initStyleOption(opt) sc = widget.style().hitTestComplexControl(QStyle.CC_ComboBox, opt, widget.mapFromGlobal(e.globalPos()), widget) if sc == QStyle.SC_ComboBoxArrow: QTimer.singleShot(0, self.hide) e.accept() return True self.hide() e.accept() return True elif etype in (e.InputMethod, e.ShortcutOverride): QApplication.sendEvent(widget, e) return False
class JobManager(QAbstractTableModel, AdaptSQP): # {{{ job_added = pyqtSignal(int) job_done = pyqtSignal(int) def __init__(self): QAbstractTableModel.__init__(self) SearchQueryParser.__init__(self, ['all']) self.wait_icon = (QIcon(I('jobs.png'))) self.running_icon = (QIcon(I('exec.png'))) self.error_icon = (QIcon(I('dialog_error.png'))) self.done_icon = (QIcon(I('ok.png'))) self.jobs = [] self.add_job = Dispatcher(self._add_job) self.server = Server(limit=int(config['worker_limit'] / 2.0), enforce_cpu_limit=config['enforce_cpu_limit']) self.threaded_server = ThreadedJobServer() self.changed_queue = Queue() self.timer = QTimer(self) self.timer.timeout.connect(self.update, type=Qt.QueuedConnection) self.timer.start(1000) def columnCount(self, parent=QModelIndex()): return 5 def rowCount(self, parent=QModelIndex()): return len(self.jobs) def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return ({ 0: _('Job'), 1: _('Status'), 2: _('Progress'), 3: _('Running time'), 4: _('Start time'), }.get(section, '')) else: return (section + 1) def show_tooltip(self, arg): widget, pos = arg QToolTip.showText(pos, self.get_tooltip()) def get_tooltip(self): running_jobs = [j for j in self.jobs if j.run_state == j.RUNNING] waiting_jobs = [j for j in self.jobs if j.run_state == j.WAITING] lines = [_('There are %d running jobs:') % len(running_jobs)] for job in running_jobs: desc = job.description if not desc: desc = _('Unknown job') p = 100. if job.is_finished else job.percent lines.append('%s: %.0f%% done' % (desc, p)) lines.extend(['', _('There are %d waiting jobs:') % len(waiting_jobs)]) for job in waiting_jobs: desc = job.description if not desc: desc = _('Unknown job') lines.append(desc) return '\n'.join(['calibre', ''] + lines) def data(self, index, role): try: if role not in (Qt.DisplayRole, Qt.DecorationRole): return None row, col = index.row(), index.column() job = self.jobs[row] if role == Qt.DisplayRole: if col == 0: desc = job.description if not desc: desc = _('Unknown job') return (desc) if col == 1: return (job.status_text) if col == 2: p = 100. if job.is_finished else job.percent return (p) if col == 3: rtime = job.running_time if rtime is None: return None return ('%dm %ds' % (int(rtime) // 60, int(rtime) % 60)) if col == 4 and job.start_time is not None: return (time.strftime('%H:%M -- %d %b', time.localtime(job.start_time))) if role == Qt.DecorationRole and col == 0: state = job.run_state if state == job.WAITING: return self.wait_icon if state == job.RUNNING: return self.running_icon if job.killed or job.failed: return self.error_icon return self.done_icon except: import traceback traceback.print_exc() return None def update(self): try: self._update() except BaseException: import traceback traceback.print_exc() def _update(self): # Update running time for i, j in enumerate(self.jobs): if j.run_state == j.RUNNING: idx = self.index(i, 3) self.dataChanged.emit(idx, idx) # Update parallel jobs jobs = set([]) while True: try: jobs.add(self.server.changed_jobs_queue.get_nowait()) except Empty: break # Update device jobs while True: try: jobs.add(self.changed_queue.get_nowait()) except Empty: break # Update threaded jobs while True: try: jobs.add(self.threaded_server.changed_jobs.get_nowait()) except Empty: break if jobs: needs_reset = False for job in jobs: orig_state = job.run_state job.update() if orig_state != job.run_state: needs_reset = True if job.is_finished: self.job_done.emit(len(self.unfinished_jobs())) if needs_reset: self.layoutAboutToBeChanged.emit() self.jobs.sort() self.layoutChanged.emit() else: for job in jobs: idx = self.jobs.index(job) self.dataChanged.emit(self.index(idx, 0), self.index(idx, 3)) # Kill parallel jobs that have gone on too long try: wmax_time = gprefs['worker_max_time'] * 60 except: wmax_time = 0 if wmax_time > 0: for job in self.jobs: if isinstance(job, ParallelJob): rtime = job.running_time if (rtime is not None and rtime > wmax_time and job.duration is None): job.timed_out = True self.server.kill_job(job) def _add_job(self, job): self.layoutAboutToBeChanged.emit() self.jobs.append(job) self.jobs.sort() self.job_added.emit(len(self.unfinished_jobs())) self.layoutChanged.emit() def done_jobs(self): return [j for j in self.jobs if j.is_finished] def unfinished_jobs(self): return [j for j in self.jobs if not j.is_finished] def row_to_job(self, row): return self.jobs[row] def has_device_jobs(self, queued_also=False): for job in self.jobs: if isinstance(job, DeviceJob): if job.duration is None: # Running or waiting if (job.is_running or queued_also): return True return False def has_jobs(self): for job in self.jobs: if job.is_running: return True return False def run_job(self, done, name, args=[], kwargs={}, description='', core_usage=1): job = ParallelJob(name, description, done, args=args, kwargs=kwargs) job.core_usage = core_usage self.add_job(job) self.server.add_job(job) return job def run_threaded_job(self, job): self.add_job(job) self.threaded_server.add_job(job) def launch_gui_app(self, name, args=[], kwargs={}, description=''): job = ParallelJob(name, description, lambda x: x, args=args, kwargs=kwargs) self.server.run_job(job, gui=True, redirect_output=False) def _kill_job(self, job): if isinstance(job, ParallelJob): self.server.kill_job(job) elif isinstance(job, ThreadedJob): self.threaded_server.kill_job(job) else: job.kill_on_start = True def hide_jobs(self, rows): for r in rows: self.jobs[r].hidden_in_gui = True for r in rows: self.dataChanged.emit(self.index(r, 0), self.index(r, 0)) def show_hidden_jobs(self): for j in self.jobs: j.hidden_in_gui = False for r in xrange(len(self.jobs)): self.dataChanged.emit(self.index(r, 0), self.index(r, 0)) def kill_job(self, row, view): job = self.jobs[row] if isinstance(job, DeviceJob): return error_dialog( view, _('Cannot kill job'), _('Cannot kill jobs that communicate with the device')).exec_( ) if job.duration is not None: return error_dialog(view, _('Cannot kill job'), _('Job has already run')).exec_() if not getattr(job, 'killable', True): return error_dialog(view, _('Cannot kill job'), _('This job cannot be stopped'), show=True) self._kill_job(job) def kill_multiple_jobs(self, rows, view): jobs = [self.jobs[row] for row in rows] devjobs = [j for j in jobs if isinstance(j, DeviceJob)] if devjobs: error_dialog( view, _('Cannot kill job'), _('Cannot kill jobs that communicate with the device')).exec_( ) jobs = [j for j in jobs if not isinstance(j, DeviceJob)] jobs = [j for j in jobs if j.duration is None] unkillable = [j for j in jobs if not getattr(j, 'killable', True)] if unkillable: names = u'\n'.join(as_unicode(j.description) for j in unkillable) error_dialog( view, _('Cannot kill job'), _('Some of the jobs cannot be stopped. Click Show details' ' to see the list of unstoppable jobs.'), det_msg=names, show=True) jobs = [j for j in jobs if getattr(j, 'killable', True)] jobs = [j for j in jobs if j.duration is None] for j in jobs: self._kill_job(j) def kill_all_jobs(self): for job in self.jobs: if (isinstance(job, DeviceJob) or job.duration is not None or not getattr(job, 'killable', True)): continue self._kill_job(job) def terminate_all_jobs(self): self.server.killall() for job in self.jobs: if (isinstance(job, DeviceJob) or job.duration is not None or not getattr(job, 'killable', True)): continue if not isinstance(job, ParallelJob): self._kill_job(job) def universal_set(self): return set([ i for i, j in enumerate(self.jobs) if not getattr(j, 'hidden_in_gui', False) ]) def get_matches(self, location, query, candidates=None): if candidates is None: candidates = self.universal_set() ans = set() if not query: return ans query = lower(query) for j in candidates: job = self.jobs[j] if job.description and query in lower(job.description): ans.add(j) return ans def find(self, query): query = query.strip() rows = self.parse(query) return rows
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 if __name__ == '__main__': from PyQt5.Qt import QTimer app = QApplication([]) d = ProgressDialog('A title', 'A message', icon='lt.png') d.show(), d.canceled_signal.connect(app.quit) QTimer.singleShot( 1000, lambda: (setattr(d, 'value', 10), setattr(d, 'msg', ('A message ' * 100)))) app.exec_()
def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.NoFrame if gprefs['tag_browser_old_look'] else QFrame.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText( '<p><b>' + _('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10, 20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) # The Alter Tag Browser button l = self.alter_tb self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate( (_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip(_('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate( (_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip( _('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item)
def display(self): if not self.isVisible(): self.show() self.raise_() QTimer.singleShot(0, self.model.build)
class TagBrowserWidget(QFrame): # {{{ def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.NoFrame if gprefs['tag_browser_old_look'] else QFrame.StyledPanel) self._parent = parent self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) # Set up the find box & button self.tb_bar = tbb = TagBrowserBar(self) self.alter_tb, self.item_search, self.search_button = tbb.alter_tb, tbb.item_search, tbb.search_button self.toggle_search_button = tbb.toggle_search_button self._layout.addWidget(tbb) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) # The tags view parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.insertWidget(0, parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText( '<p><b>' + _('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10, 20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) # The Alter Tag Browser button l = self.alter_tb self.collapse_all_action = ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser collapse all', _('Collapse all'), default_keys=(), action=ac, group=_('Tag browser')) connect_lambda(ac.triggered, self, lambda self: self.tags_view.collapseAll()) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser alter', _('Configure Tag browser'), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(l.showMenu) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate( (_('Name'), _('Number of books'), _('Average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip(_('Set the sort order for entries in the Tag browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate( (_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip( _('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', _("'Click' found item"), default_keys=(), action=ac, group=_('Tag browser')) ac.triggered.connect(self.toggle_item) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def save_state(self): gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked()) def toggle_item(self): self.tags_view.toggle_current_index() def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): self.current_find_position = None def set_focus_to_find_box(self): self.tb_bar.set_focus_to_find_box() def do_find(self, str=None): self.current_find_position = None self.find() @property def find_text(self): return unicode(self.item_search.currentText()).strip() def find(self): model = self.tags_view.model() model.clear_boxed() txt = self.find_text if txt.startswith('*'): model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) key = None colon = txt.rfind(':') if len(txt) > 2 else 0 if colon > 0: key = self._parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon + 1:] self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def keyPressEvent(self, ev): if ev.key() in (Qt.Key_Enter, Qt.Key_Return) and self.find_text: self.find() ev.accept() return return QFrame.keyPressEvent(self, ev)
class MaterialManager(QObject): materialsUpdated = pyqtSignal( ) # Emitted whenever the material lookup tables are updated. favoritesUpdated = pyqtSignal( ) # Emitted whenever the favorites are changed def __init__(self, container_registry, parent=None): super().__init__(parent) self._application = Application.getInstance() self._container_registry = container_registry # type: ContainerRegistry # Material_type -> generic material metadata self._fallback_materials_map = dict( ) # type: Dict[str, Dict[str, Any]] # Root_material_id -> MaterialGroup self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Approximate diameter str self._diameter_machine_nozzle_buildplate_material_map = dict( ) # type: Dict[str, Dict[str, MaterialNode]] # We're using these two maps to convert between the specific diameter material id and the generic material id # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # i.e. generic_pla -> generic_pla_175 # root_material_id -> approximate diameter str -> root_material_id for that diameter self._material_diameter_map = defaultdict( dict) # type: Dict[str, Dict[str, str]] # Material id including diameter (generic_pla_175) -> material root id (generic_pla) self._diameter_material_map = dict() # type: Dict[str, str] # This is used in Legacy UM3 send material function and the material management page. # GUID -> a list of material_groups self._guid_material_groups_map = defaultdict( list) # type: Dict[str, List[MaterialGroup]] # The machine definition ID for the non-machine-specific materials. # This is used as the last fallback option if the given machine-specific material(s) cannot be found. self._default_machine_definition_id = "fdmprinter" self._default_approximate_diameter_for_quality_search = "3" # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't # react too many time. self._update_timer = QTimer(self) self._update_timer.setInterval(300) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._updateMaps) self._container_registry.containerMetaDataChanged.connect( self._onContainerMetadataChanged) self._container_registry.containerAdded.connect( self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect( self._onContainerMetadataChanged) self._favorites = set() # type: Set[str] def initialize(self) -> None: # Find all materials and put them in a matrix for quick search. material_metadatas = { metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata( type="material") if metadata.get("GUID") } # type: Dict[str, Dict[str, Any]] self._material_group_map = dict() # type: Dict[str, MaterialGroup] # Map #1 # root_material_id -> MaterialGroup for material_id, material_metadata in material_metadatas.items(): # We don't store empty material in the lookup tables if material_id == "empty_material": continue root_material_id = material_metadata.get("base_file", "") if root_material_id not in self._material_group_map: self._material_group_map[root_material_id] = MaterialGroup( root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[ root_material_id].is_read_only = self._container_registry.isReadOnly( root_material_id) group = self._material_group_map[root_material_id] # Store this material in the group of the appropriate root material. if material_id != root_material_id: new_node = MaterialNode(material_metadata) group.derived_material_node_list.append(new_node) # Order this map alphabetically so it's easier to navigate in a debugger self._material_group_map = OrderedDict( sorted(self._material_group_map.items(), key=lambda x: x[0])) # Map #1.5 # GUID -> material group list self._guid_material_groups_map = defaultdict( list) # type: Dict[str, List[MaterialGroup]] for root_material_id, material_group in self._material_group_map.items( ): guid = material_group.root_material_node.getMetaDataEntry( "GUID", "") self._guid_material_groups_map[guid].append(material_group) # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() # type: Dict[str, Any] material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items( ): material_type = material_node.root_material_node.getMetaDataEntry( "material", "") if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = { "generic": None, "others": [] } material_types_without_fallback.add(material_type) brand = material_node.root_material_node.getMetaDataEntry( "brand", "") if brand.lower() == "generic": to_add = True if material_type in grouped_by_type_dict: diameter = material_node.root_material_node.getMetaDataEntry( "approximate_diameter", "") if diameter != self._default_approximate_diameter_for_quality_search: to_add = False # don't add if it's not the default diameter if to_add: # Checking this first allow us to differentiate between not read only materials: # - if it's in the list, it means that is a new material without fallback # - if it is not, then it is a custom material with a fallback material (parent) if material_type in material_types_without_fallback: grouped_by_type_dict[ material_type] = material_node.root_material_node._metadata material_types_without_fallback.remove(material_type) # Remove the materials that have no fallback materials for material_type in material_types_without_fallback: del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 # There can be multiple material profiles for the same material with different diameters, such as "generic_pla" # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID # for quality search. self._material_diameter_map = defaultdict(dict) self._diameter_material_map = dict() # Group the material IDs by the same name, material, brand, and color but with different diameters. material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]] keys_to_fetch = ("name", "material", "brand", "color") for root_material_id, machine_node in self._material_group_map.items(): root_material_metadata = machine_node.root_material_node._metadata key_data_list = [] # type: List[Any] for key in keys_to_fetch: key_data_list.append( machine_node.root_material_node.getMetaDataEntry(key)) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any] # If the key_data doesn't exist, it doesn't matter if the material is read only... if key_data not in material_group_dict: material_group_dict[key_data] = dict() else: # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it if not machine_node.is_read_only: continue approximate_diameter = machine_node.root_material_node.getMetaDataEntry( "approximate_diameter", "") material_group_dict[key_data][ approximate_diameter] = machine_node.root_material_node.getMetaDataEntry( "id", "") # Map [root_material_id][diameter] -> root_material_id for this diameter for data_dict in material_group_dict.values(): for root_material_id1 in data_dict.values(): if root_material_id1 in self._material_diameter_map: continue diameter_map = data_dict for root_material_id2 in data_dict.values(): self._material_diameter_map[ root_material_id2] = diameter_map default_root_material_id = data_dict.get( self._default_approximate_diameter_for_quality_search) if default_root_material_id is None: default_root_material_id = list(data_dict.values())[ 0] # no default diameter present, just take "the" only one for root_material_id in data_dict.values(): self._diameter_material_map[ root_material_id] = default_root_material_id # Map #4 # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_nozzle_buildplate_material_map = dict( ) # type: Dict[str, Dict[str, MaterialNode]] for material_metadata in material_metadatas.values(): self.__addMaterialMetadataIntoLookupTree(material_metadata) self.materialsUpdated.emit() favorites = self._application.getPreferences().getValue( "cura/favorite_materials") for item in favorites.split(";"): self._favorites.add(item) self.favoritesUpdated.emit() def __addMaterialMetadataIntoLookupTree( self, material_metadata: Dict[str, Any]) -> None: material_id = material_metadata["id"] # We don't store empty material in the lookup tables if material_id == "empty_material": return root_material_id = material_metadata["base_file"] definition = material_metadata["definition"] approximate_diameter = material_metadata["approximate_diameter"] if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: self._diameter_machine_nozzle_buildplate_material_map[ approximate_diameter] = {} machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[ approximate_diameter] if definition not in machine_nozzle_buildplate_material_map: machine_nozzle_buildplate_material_map[definition] = MaterialNode() # This is a list of information regarding the intermediate nodes: # nozzle -> buildplate nozzle_name = material_metadata.get("variant_name") buildplate_name = material_metadata.get("buildplate_name") intermediate_node_info_list = [ (nozzle_name, VariantType.NOZZLE), (buildplate_name, VariantType.BUILD_PLATE), ] variant_manager = self._application.getVariantManager() machine_node = machine_nozzle_buildplate_material_map[definition] current_node = machine_node current_intermediate_node_info_idx = 0 error_message = None # type: Optional[str] while current_intermediate_node_info_idx < len( intermediate_node_info_list): variant_name, variant_type = intermediate_node_info_list[ current_intermediate_node_info_idx] if variant_name is not None: # The new material has a specific variant, so it needs to be added to that specific branch in the tree. variant = variant_manager.getVariantNode( definition, variant_name, variant_type) if variant is None: error_message = "Material {id} contains a variant {name} that does not exist.".format( id=material_metadata["id"], name=variant_name) break # Update the current node to advance to a more specific branch if variant_name not in current_node.children_map: current_node.children_map[variant_name] = MaterialNode() current_node = current_node.children_map[variant_name] current_intermediate_node_info_idx += 1 if error_message is not None: Logger.log( "e", "%s It will not be added into the material lookup tree.", error_message) self._container_registry.addWrongContainerId( material_metadata["id"]) return # Add the material to the current tree node, which is the deepest (the most specific) branch we can find. # Sanity check: Make sure that there is no duplicated materials. if root_material_id in current_node.material_map: Logger.log( "e", "Duplicated material [%s] with root ID [%s]. It has already been added.", material_id, root_material_id) ConfigurationErrorMessage.getInstance().addFaultyContainers( root_material_id) return current_node.material_map[root_material_id] = MaterialNode( material_metadata) def _updateMaps(self): Logger.log("i", "Updating material lookup data ...") self.initialize() def _onContainerMetadataChanged(self, container): self._onContainerChanged(container) def _onContainerChanged(self, container): container_type = container.getMetaDataEntry("type") if container_type != "material": return # update the maps self._update_timer.start() def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]: return self._material_group_map.get(root_material_id) def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str: return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: return self._diameter_material_map.get(root_material_id, "") def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: return self._guid_material_groups_map.get(guid) # # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log( "i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) return dict() machine_definition_id = machine_definition.getId() # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[ rounded_diameter] machine_node = machine_nozzle_buildplate_material_map.get( machine_definition_id) default_machine_node = machine_nozzle_buildplate_material_map.get( self._default_machine_definition_id) nozzle_node = None buildplate_node = None if nozzle_name is not None and machine_node is not None: nozzle_node = machine_node.getChildNode(nozzle_name) # Get buildplate node if possible if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) nodes_to_check = [ buildplate_node, nozzle_node, machine_node, default_machine_node ] # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry( "exclude_materials", []) material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] for current_node in nodes_to_check: if current_node is None: continue # Only exclude the materials that are explicitly specified in the "exclude_materials" field. # Do not exclude other materials that are of the same type. for material_id, node in current_node.material_map.items(): if material_id in machine_exclude_materials: Logger.log("d", "Exclude material [%s] for machine [%s]", material_id, machine_definition.getId()) continue if material_id not in material_id_metadata_dict: material_id_metadata_dict[material_id] = node return material_id_metadata_dict # # A convenience function to get available materials for the given machine with the extruder position. # def getAvailableMaterialsForMachineExtruder( self, machine: "GlobalStack", extruder_stack: "ExtruderStack" ) -> Optional[Dict[str, MaterialNode]]: buildplate_name = machine.getBuildplateName() nozzle_name = None if extruder_stack.variant.getId() != "empty_variant": nozzle_name = extruder_stack.variant.getName() diameter = extruder_stack.approximateMaterialDiameter # Fetch the available materials (ContainerNode) for the current active machine and extruder setup. return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter) # # Gets MaterialNode for the given extruder and machine with the given material name. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: Logger.log( "i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", diameter, rounded_diameter, root_material_id) return None # If there are nozzle materials, get the nozzle-specific material machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[ rounded_diameter] # type: Dict[str, MaterialNode] machine_node = machine_nozzle_buildplate_material_map.get( machine_definition_id) nozzle_node = None buildplate_node = None # Fallback for "fdmprinter" if the machine-specific materials cannot be found if machine_node is None: machine_node = machine_nozzle_buildplate_material_map.get( self._default_machine_definition_id) if machine_node is not None and nozzle_name is not None: nozzle_node = machine_node.getChildNode(nozzle_name) if nozzle_node is not None and buildplate_name is not None: buildplate_node = nozzle_node.getChildNode(buildplate_name) # Fallback mechanism of finding materials: # 1. buildplate-specific material # 2. nozzle-specific material # 3. machine-specific material # 4. generic material (for fdmprinter) nodes_to_check = [ buildplate_node, nozzle_node, machine_node, machine_nozzle_buildplate_material_map.get( self._default_machine_definition_id) ] material_node = None for node in nodes_to_check: if node is not None: material_node = node.material_map.get(root_material_id) if material_node: break return material_node # # Gets MaterialNode for the given extruder and machine with the given material type. # Returns None if: # 1. the given machine doesn't have materials; # 2. cannot find any material InstanceContainers with the given settings. # def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str, buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]: node = None machine_definition = global_stack.definition extruder_definition = global_stack.extruders[position].definition if parseBool( machine_definition.getMetaDataEntry("has_materials", False)): material_diameter = extruder_definition.getProperty( "material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) # Look at the guid to material dictionary root_material_id = None for material_group in self._guid_material_groups_map[ material_guid]: root_material_id = cast( str, material_group.root_material_node.getMetaDataEntry( "id", "")) break if not root_material_id: Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) return None node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node # # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla". # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use # the generic material IDs to search for qualities. # # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine. # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will # be "generic_pla". This function is intended to get a generic fallback material for the given material type. # # This function returns the generic root material ID for the given material type, where material types are "PLA", # "ABS", etc. # def getFallbackMaterialIdByMaterialType( self, material_type: str) -> Optional[str]: # For safety if material_type not in self._fallback_materials_map: Logger.log( "w", "The material type [%s] does not have a fallback material" % material_type) return None fallback_material = self._fallback_materials_map[material_type] if fallback_material: return self.getRootMaterialIDWithoutDiameter( fallback_material["id"]) else: return None ## Get default material for given global stack, extruder position and extruder nozzle name # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) def getDefaultMaterial( self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None ) -> Optional["MaterialNode"]: node = None buildplate_name = global_stack.getBuildplateName() machine_definition = global_stack.definition if extruder_definition is None: extruder_definition = global_stack.extruders[position].definition if extruder_definition and parseBool( global_stack.getMetaDataEntry("has_materials", False)): # At this point the extruder_definition is not None material_diameter = extruder_definition.getProperty( "material_diameter", "value") if isinstance(material_diameter, SettingFunction): material_diameter = material_diameter(global_stack) approximate_material_diameter = str(round(material_diameter)) root_material_id = machine_definition.getMetaDataEntry( "preferred_material") root_material_id = self.getRootMaterialIDForDiameter( root_material_id, approximate_material_diameter) node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name, material_diameter, root_material_id) return node def removeMaterialByRootId(self, root_material_id: str): material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log( "i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id) return nodes_to_remove = [material_group.root_material_node ] + material_group.derived_material_node_list for node in nodes_to_remove: self._container_registry.removeContainer( node.getMetaDataEntry("id", "")) # # Methods for GUI # # # Sets the new name for the given material. # @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is None: return if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) return material_group = self.getMaterialGroup(root_material_id) if material_group: container = material_group.root_material_node.getContainer() if container: container.setName(name) # # Removes the given material. # @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: root_material_id = material_node.getMetaDataEntry("base_file") if root_material_id is not None: self.removeMaterialByRootId(root_material_id) # # Creates a duplicate of a material, which has the same GUID and base_file metadata. # Returns the root material ID of the duplicated material if successful. # @pyqtSlot("QVariant", result=str) def duplicateMaterial( self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]: root_material_id = cast( str, material_node.getMetaDataEntry("base_file", "")) material_group = self.getMaterialGroup(root_material_id) if not material_group: Logger.log( "i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id) return None base_container = material_group.root_material_node.getContainer() if not base_container: return None # Ensure all settings are saved. self._application.saveSettings() # Create a new ID & container to hold the data. new_containers = [] if new_base_id is None: new_base_id = self._container_registry.uniqueName( base_container.getId()) new_base_container = copy.deepcopy(base_container) new_base_container.getMetaData()["id"] = new_base_id new_base_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_base_container.getMetaData()[key] = value new_containers.append(new_base_container) # Clone all of them. for node in material_group.derived_material_node_list: container_to_copy = node.getContainer() if not container_to_copy: continue # Create unique IDs for every clone. new_id = new_base_id if container_to_copy.getMetaDataEntry( "definition") != "fdmprinter": new_id += "_" + container_to_copy.getMetaDataEntry( "definition") if container_to_copy.getMetaDataEntry("variant_name"): nozzle_name = container_to_copy.getMetaDataEntry( "variant_name") new_id += "_" + nozzle_name.replace(" ", "_") new_container = copy.deepcopy(container_to_copy) new_container.getMetaData()["id"] = new_id new_container.getMetaData()["base_file"] = new_base_id if new_metadata is not None: for key, value in new_metadata.items(): new_container.getMetaData()[key] = value new_containers.append(new_container) for container_to_add in new_containers: container_to_add.setDirty(True) self._container_registry.addContainer(container_to_add) # if the duplicated material was favorite then the new material should also be added to favorite. if root_material_id in self.getFavorites(): self.addFavorite(new_base_id) return new_base_id # # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Returns the ID of the newly created material. @pyqtSlot(result=str) def createMaterial(self) -> str: from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") # Ensure all settings are saved. self._application.saveSettings() machine_manager = self._application.getMachineManager() extruder_stack = machine_manager.activeStack approximate_diameter = str(extruder_stack.approximateMaterialDiameter) root_material_id = "generic_pla" root_material_id = self.getRootMaterialIDForDiameter( root_material_id, approximate_diameter) material_group = self.getMaterialGroup(root_material_id) if not material_group: # This should never happen Logger.log("w", "Cannot get the material group of %s.", root_material_id) return "" # Create a new ID & container to hold the data. new_id = self._container_registry.uniqueName("custom_material") new_metadata = { "name": catalog.i18nc("@label", "Custom Material"), "brand": catalog.i18nc("@label", "Custom"), "GUID": str(uuid.uuid4()), } self.duplicateMaterial(material_group.root_material_node, new_base_id=new_id, new_metadata=new_metadata) return new_id @pyqtSlot(str) def addFavorite(self, root_material_id: str) -> None: self._favorites.add(root_material_id) self.favoritesUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue( "cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot(str) def removeFavorite(self, root_material_id: str) -> None: self._favorites.remove(root_material_id) self.favoritesUpdated.emit() # Ensure all settings are saved. self._application.getPreferences().setValue( "cura/favorite_materials", ";".join(list(self._favorites))) self._application.saveSettings() @pyqtSlot() def getFavorites(self): return self._favorites
def __init__(self, parent, db, id_to_select, select_sort, select_link): QDialog.__init__(self, parent) Ui_EditAuthorsDialog.__init__(self) self.setupUi(self) # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) try: self.table_column_widths = \ gprefs.get('manage_authors_table_widths', None) geom = gprefs.get('manage_authors_dialog_geometry', bytearray('')) self.restoreGeometry(QByteArray(geom)) except: pass self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) # Set up the column headings self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setColumnCount(3) self.down_arrow_icon = QIcon(I('arrow-down.png')) self.up_arrow_icon = QIcon(I('arrow-up.png')) self.blank_icon = QIcon(I('blank.png')) self.auth_col = QTableWidgetItem(_('Author')) self.table.setHorizontalHeaderItem(0, self.auth_col) self.auth_col.setIcon(self.blank_icon) self.aus_col = QTableWidgetItem(_('Author sort')) self.table.setHorizontalHeaderItem(1, self.aus_col) self.aus_col.setIcon(self.up_arrow_icon) self.aul_col = QTableWidgetItem(_('Link')) self.table.setHorizontalHeaderItem(2, self.aul_col) self.aus_col.setIcon(self.blank_icon) # Add the data self.authors = {} auts = db.get_authors_with_ids() self.table.setRowCount(len(auts)) select_item = None for row, (id, author, sort, link) in enumerate(auts): author = author.replace('|', ',') self.authors[id] = (author, sort, link) aut = tableItem(author) aut.setData(Qt.UserRole, id) sort = tableItem(sort) link = tableItem(link) self.table.setItem(row, 0, aut) self.table.setItem(row, 1, sort) self.table.setItem(row, 2, link) if id_to_select in (id, author): if select_sort: select_item = sort elif select_link: select_item = link else: select_item = aut self.table.resizeColumnsToContents() if self.table.columnWidth(2) < 200: self.table.setColumnWidth(2, 200) # set up the cellChanged signal only after the table is filled self.table.cellChanged.connect(self.cell_changed) # set up sort buttons self.sort_by_author.setCheckable(True) self.sort_by_author.setChecked(False) self.sort_by_author.clicked.connect(self.do_sort_by_author) self.author_order = 1 self.sort_by_author_sort.clicked.connect(self.do_sort_by_author_sort) self.sort_by_author_sort.setCheckable(True) self.sort_by_author_sort.setChecked(True) self.author_sort_order = 1 self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort) self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author) # Position on the desired item if select_item is not None: self.table.setCurrentItem(select_item) self.table.editItem(select_item) self.start_find_pos = select_item.row() * 2 + select_item.column() else: self.table.setCurrentCell(0, 0) self.start_find_pos = -1 # set up the search box self.find_box.initialize('manage_authors_search') self.find_box.lineEdit().returnPressed.connect(self.do_find) self.find_box.editTextChanged.connect(self.find_text_changed) self.find_button.clicked.connect(self.do_find) self.find_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label.move(40, 40) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) self.do_sort_by_author_sort()
class Model_View(Model): def __init__(self): super(Model_View, self).__init__(databasetype="QSQLITE", databasename="Sqlite_Sql/testcase.db", sqltablename="result") #讲所有测试用例的 def modelview(self): self.view = QTableView() self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) self.view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.view.setSelectionBehavior(QAbstractItemView.SelectRows) self.view.setModel(self.model) for index in range(self.model.rowCount()): case_name = self.model.data(self.model.index(index, 2)) self.view.setIndexWidget(self.model.index(index, 6), self.button(case_name)) # self.view.setIndexWidget(self.model.index(index, 5), self.time(case_name)) return self.view def button(self, id): widget = QWidget() Btn = QPushButton('执行') Btn.setStyleSheet(''' text-align : center; background-color : NavajoWhite; height : 30px; border-style: outset; font : 15px ''') Btn.clicked.connect(lambda: self.Work(id)) self.timer = QTimer() # self.timer.timeout.connect(self.CountTime) hLayout = QHBoxLayout() hLayout.addWidget(Btn) hLayout.setContentsMargins(0, 0, 0, 0) widget.setLayout(hLayout) return widget # def time(self,case_name): # # return QLCDNumber() # def CountTime(self): # self.t += 1 # self.time(self.name).display(self.t) def Work(self, index): self.name = index self.timer.start(1000) self.thread = RunThread(index) self.thread.start() self.thread.thread_signal.connect(self.TimeStop) # @staticmethod # def allrun(): # # thread = RunThread(0) # thread.start() # # thread.join() # QCoreApplication.processEvents() def TimeStop(self): self.timer.stop() LOGER.loginfo(self.name + "用例执行完毕,邮件已发送") print("用例运行完毕,邮件已发送")
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def __init__(self, parent, db, id_to_select, select_sort, select_link): QDialog.__init__(self, parent) Ui_EditAuthorsDialog.__init__(self) self.setupUi(self) # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowIcon(icon) try: self.table_column_widths = \ gprefs.get('manage_authors_table_widths', None) geom = gprefs.get('manage_authors_dialog_geometry', bytearray('')) self.restoreGeometry(QByteArray(geom)) except: pass self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.accepted.connect(self.accepted) # Set up the column headings self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setColumnCount(3) self.down_arrow_icon = QIcon(I('arrow-down.png')) self.up_arrow_icon = QIcon(I('arrow-up.png')) self.blank_icon = QIcon(I('blank.png')) self.auth_col = QTableWidgetItem(_('Author')) self.table.setHorizontalHeaderItem(0, self.auth_col) self.auth_col.setIcon(self.blank_icon) self.aus_col = QTableWidgetItem(_('Author sort')) self.table.setHorizontalHeaderItem(1, self.aus_col) self.aus_col.setIcon(self.up_arrow_icon) self.aul_col = QTableWidgetItem(_('Link')) self.table.setHorizontalHeaderItem(2, self.aul_col) self.aus_col.setIcon(self.blank_icon) # Add the data self.authors = {} auts = db.get_authors_with_ids() self.table.setRowCount(len(auts)) select_item = None for row, (id, author, sort, link) in enumerate(auts): author = author.replace('|', ',') self.authors[id] = (author, sort, link) aut = tableItem(author) aut.setData(Qt.UserRole, id) sort = tableItem(sort) link = tableItem(link) self.table.setItem(row, 0, aut) self.table.setItem(row, 1, sort) self.table.setItem(row, 2, link) if id_to_select in (id, author): if select_sort: select_item = sort elif select_link: select_item = link else: select_item = aut self.table.resizeColumnsToContents() if self.table.columnWidth(2) < 200: self.table.setColumnWidth(2, 200) # set up the cellChanged signal only after the table is filled self.table.cellChanged.connect(self.cell_changed) # set up sort buttons self.sort_by_author.setCheckable(True) self.sort_by_author.setChecked(False) self.sort_by_author.clicked.connect(self.do_sort_by_author) self.author_order = 1 self.sort_by_author_sort.clicked.connect(self.do_sort_by_author_sort) self.sort_by_author_sort.setCheckable(True) self.sort_by_author_sort.setChecked(True) self.author_sort_order = 1 self.recalc_author_sort.clicked.connect(self.do_recalc_author_sort) self.auth_sort_to_author.clicked.connect(self.do_auth_sort_to_author) # Position on the desired item if select_item is not None: self.table.setCurrentItem(select_item) self.table.editItem(select_item) self.start_find_pos = select_item.row() * 2 + select_item.column() else: self.table.setCurrentCell(0, 0) self.start_find_pos = -1 # set up the search box self.find_box.initialize('manage_authors_search') self.find_box.lineEdit().returnPressed.connect(self.do_find) self.find_box.editTextChanged.connect(self.find_text_changed) self.find_button.clicked.connect(self.do_find) self.find_button.setDefault(True) l = QLabel(self.table) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No matches found')) l.setAlignment(Qt.AlignVCenter) l.resize(l.sizeHint()) l.move(10,20) l.setVisible(False) self.not_found_label.move(40, 40) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) self.do_sort_by_author_sort() def save_state(self): self.table_column_widths = [] for c in range(0, self.table.columnCount()): self.table_column_widths.append(self.table.columnWidth(c)) gprefs['manage_authors_table_widths'] = self.table_column_widths gprefs['manage_authors_dialog_geometry'] = bytearray(self.saveGeometry()) def resizeEvent(self, *args): QDialog.resizeEvent(self, *args) if self.table_column_widths is not None: for c,w in enumerate(self.table_column_widths): self.table.setColumnWidth(c, w) else: # the vertical scroll bar might not be rendered, so might not yet # have a width. Assume 25. Not a problem because user-changed column # widths will be remembered w = self.table.width() - 25 - self.table.verticalHeader().width() w /= self.table.columnCount() for c in range(0, self.table.columnCount()): self.table.setColumnWidth(c, w) self.save_state() def show_context_menu(self, point): self.context_item = self.table.itemAt(point) case_menu = QMenu(_('Change case')) action_upper_case = case_menu.addAction(_('Upper case')) action_lower_case = case_menu.addAction(_('Lower case')) action_swap_case = case_menu.addAction(_('Swap case')) action_title_case = case_menu.addAction(_('Title case')) action_capitalize = case_menu.addAction(_('Capitalize')) action_upper_case.triggered.connect(self.upper_case) action_lower_case.triggered.connect(self.lower_case) action_swap_case.triggered.connect(self.swap_case) action_title_case.triggered.connect(self.title_case) action_capitalize.triggered.connect(self.capitalize) m = self.au_context_menu = QMenu() ca = m.addAction(_('Copy')) ca.triggered.connect(self.copy_to_clipboard) ca = m.addAction(_('Paste')) ca.triggered.connect(self.paste_from_clipboard) m.addSeparator() if self.context_item is not None and self.context_item.column() == 0: ca = m.addAction(_('Copy to author sort')) ca.triggered.connect(self.copy_au_to_aus) m.addSeparator() ca = m.addAction(_("Show books by author in book list")) ca.triggered.connect(self.search) else: ca = m.addAction(_('Copy to author')) ca.triggered.connect(self.copy_aus_to_au) m.addSeparator() m.addMenu(case_menu) m.exec_(self.table.mapToGlobal(point)) def search(self): from calibre.gui2.ui import get_gui row = self.context_item.row() get_gui().search.set_search_string(self.table.item(row, 0).text()) def copy_to_clipboard(self): cb = QApplication.clipboard() cb.setText(unicode_type(self.context_item.text())) def paste_from_clipboard(self): cb = QApplication.clipboard() self.context_item.setText(cb.text()) def upper_case(self): self.context_item.setText(icu_upper(unicode_type(self.context_item.text()))) def lower_case(self): self.context_item.setText(icu_lower(unicode_type(self.context_item.text()))) def swap_case(self): self.context_item.setText(unicode_type(self.context_item.text()).swapcase()) def title_case(self): from calibre.utils.titlecase import titlecase self.context_item.setText(titlecase(unicode_type(self.context_item.text()))) def capitalize(self): from calibre.utils.icu import capitalize self.context_item.setText(capitalize(unicode_type(self.context_item.text()))) def copy_aus_to_au(self): row = self.context_item.row() dest = self.table.item(row, 0) dest.setText(self.context_item.text()) def copy_au_to_aus(self): row = self.context_item.row() dest = self.table.item(row, 1) dest.setText(self.context_item.text()) def not_found_label_timer_event(self): self.not_found_label.setVisible(False) def find_text_changed(self): self.start_find_pos = -1 def do_find(self): self.not_found_label.setVisible(False) # For some reason the button box keeps stealing the RETURN shortcut. # Steal it back self.buttonBox.button(QDialogButtonBox.Ok).setDefault(False) self.buttonBox.button(QDialogButtonBox.Ok).setAutoDefault(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(False) self.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) st = icu_lower(unicode_type(self.find_box.currentText())) for i in range(0, self.table.rowCount()*2): self.start_find_pos = (self.start_find_pos + 1) % (self.table.rowCount()*2) r = (self.start_find_pos/2)%self.table.rowCount() c = self.start_find_pos % 2 item = self.table.item(r, c) text = icu_lower(unicode_type(item.text())) if st in text: self.table.setCurrentItem(item) self.table.setFocus(True) return # Nothing found. Pop up the little dialog for 1.5 seconds self.not_found_label.setVisible(True) self.not_found_label_timer.start(1500) def do_sort_by_author(self): self.author_order = 1 if self.author_order == 0 else 0 self.table.sortByColumn(0, self.author_order) self.sort_by_author.setChecked(True) self.sort_by_author_sort.setChecked(False) self.auth_col.setIcon(self.down_arrow_icon if self.author_order else self.up_arrow_icon) self.aus_col.setIcon(self.blank_icon) def do_sort_by_author_sort(self): self.author_sort_order = 1 if self.author_sort_order == 0 else 0 self.table.sortByColumn(1, self.author_sort_order) self.sort_by_author.setChecked(False) self.sort_by_author_sort.setChecked(True) self.aus_col.setIcon(self.down_arrow_icon if self.author_sort_order else self.up_arrow_icon) self.auth_col.setIcon(self.blank_icon) def accepted(self): self.save_state() self.result = [] for row in range(0,self.table.rowCount()): id = int(self.table.item(row, 0).data(Qt.UserRole)) aut = unicode_type(self.table.item(row, 0).text()).strip() sort = unicode_type(self.table.item(row, 1).text()).strip() link = unicode_type(self.table.item(row, 2).text()).strip() orig_aut,orig_sort,orig_link = self.authors[id] if orig_aut != aut or orig_sort != sort or orig_link != link: self.result.append((id, orig_aut, aut, sort, link)) def do_recalc_author_sort(self): self.table.cellChanged.disconnect() for row in range(0,self.table.rowCount()): item = self.table.item(row, 0) aut = unicode_type(item.text()).strip() c = self.table.item(row, 1) # Sometimes trailing commas are left by changing between copy algs c.setText(author_to_author_sort(aut).rstrip(',')) self.table.setFocus(Qt.OtherFocusReason) self.table.cellChanged.connect(self.cell_changed) def do_auth_sort_to_author(self): self.table.cellChanged.disconnect() for row in range(0,self.table.rowCount()): item = self.table.item(row, 1) aus = unicode_type(item.text()).strip() c = self.table.item(row, 0) # Sometimes trailing commas are left by changing between copy algs c.setText(aus) self.table.setFocus(Qt.OtherFocusReason) self.table.cellChanged.connect(self.cell_changed) def cell_changed(self, row, col): if col == 0: item = self.table.item(row, 0) aut = unicode_type(item.text()).strip() aut_list = string_to_authors(aut) if len(aut_list) != 1: error_dialog(self.parent(), _('Invalid author name'), _('You cannot change an author to multiple authors.')).exec_() aut = ' % '.join(aut_list) self.table.item(row, 0).setText(aut) c = self.table.item(row, 1) c.setText(author_to_author_sort(aut)) item = c else: item = self.table.item(row, col) self.table.setCurrentItem(item) self.table.scrollToItem(item)
class TagBrowserWidget(QWidget): # {{{ def __init__(self, parent): QWidget.__init__(self, parent) self.parent = parent self._layout = QVBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(0, 0, 0, 0) # Set up the find box & button search_layout = QHBoxLayout() self._layout.addLayout(search_layout) self.item_search = HistoryLineEdit(parent) self.item_search.setMinimumContentsLength(5) self.item_search.setSizeAdjustPolicy( self.item_search.AdjustToMinimumContentsLengthWithIcon) try: self.item_search.lineEdit().setPlaceholderText( _('Find item in tag browser')) except: pass # Using Qt < 4.7 self.item_search.setToolTip( _('Search for items. This is a "contains" search; items containing the\n' 'text anywhere in the name will be found. You can limit the search\n' 'to particular categories using syntax similar to search. For example,\n' 'tags:foo will find foo in any tag, but not in authors etc. Entering\n' '*foo will filter all categories at once, showing only those items\n' 'containing the text "foo"')) search_layout.addWidget(self.item_search) # Not sure if the shortcut should be translatable ... sc = QShortcut(QKeySequence(_('ALT+f')), parent) sc.activated.connect(self.set_focus_to_find_box) self.search_button = QToolButton() self.search_button.setText(_('F&ind')) self.search_button.setToolTip(_('Find the first/next matching item')) search_layout.addWidget(self.search_button) self.expand_button = QToolButton() self.expand_button.setText('-') self.expand_button.setToolTip(_('Collapse all categories')) search_layout.addWidget(self.expand_button) search_layout.setStretch(0, 10) search_layout.setStretch(1, 1) search_layout.setStretch(2, 1) self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.initialize('tag_browser_search') self.item_search.lineEdit().returnPressed.connect(self.do_find) self.item_search.lineEdit().textEdited.connect(self.find_text_changed) self.item_search.activated[str].connect(self.do_find) self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive) parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self.expand_button.clicked.connect(self.tags_view.collapseAll) self._layout.addWidget(parent.tags_view) # Now the floating 'not found' box l = QLabel(self.tags_view) self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText( '<p><b>' + _('No More Matches.</b><p> Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) l.move(10, 20) l.setVisible(False) self.not_found_label_timer = QTimer() self.not_found_label_timer.setSingleShot(True) self.not_found_label_timer.timeout.connect( self.not_found_label_timer_event, type=Qt.QueuedConnection) parent.alter_tb = l = QPushButton(parent) l.setText(_('Alter Tag Browser')) l.setIcon(QIcon(I('tags.png'))) l.m = QMenu() l.setMenu(l.m) self._layout.addWidget(l) sb = l.m.addAction(_('Sort by')) sb.m = l.sort_menu = QMenu(l.m) sb.setMenu(sb.m) sb.bg = QActionGroup(sb) # Must be in the same order as db2.CATEGORY_SORTS for i, x in enumerate((_('Sort by name'), _('Sort by number of books'), _('Sort by average rating'))): a = sb.m.addAction(x) sb.bg.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) sb.setToolTip(_('Set the sort order for entries in the Tag Browser')) sb.setStatusTip(sb.toolTip()) ma = l.m.addAction(_('Search type when selecting multiple items')) ma.m = l.match_menu = QMenu(l.m) ma.setMenu(ma.m) ma.ag = QActionGroup(ma) # Must be in the same order as db2.MATCH_TYPE for i, x in enumerate( (_('Match any of the items'), _('Match all of the items'))): a = ma.m.addAction(x) ma.ag.addAction(a) a.setCheckable(True) if i == 0: a.setChecked(True) ma.setToolTip( _('When selecting multiple entries in the Tag Browser ' 'match any or all of them')) ma.setStatusTip(ma.toolTip()) mt = l.m.addAction(_('Manage authors, tags, etc.')) mt.setToolTip( _('All of these category_managers are available by right-clicking ' 'on items in the tag browser above')) mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) # self.leak_test_timer.start(5000) def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): self.current_find_position = None def set_focus_to_find_box(self): self.item_search.setFocus() self.item_search.lineEdit().selectAll() def do_find(self, str=None): self.current_find_position = None self.find() def find(self): model = self.tags_view.model() model.clear_boxed() txt = unicode(self.item_search.currentText()).strip() if txt.startswith('*'): model.set_categories_filter(txt[1:]) self.tags_view.recount() self.current_find_position = None return if model.get_categories_filter(): model.set_categories_filter(None) self.tags_view.recount() self.current_find_position = None if not txt: return self.item_search.lineEdit().blockSignals(True) self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) key = None colon = txt.rfind(':') if len(txt) > 2 else 0 if colon > 0: key = self.parent.library_view.model().db.\ field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon + 1:] self.current_find_position = \ model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: self.tags_view.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) if self.tags_view.verticalScrollBar().isVisible(): sbw = self.tags_view.verticalScrollBar().width() else: sbw = 0 width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): self.not_found_label.setVisible(False)
def check_exited(self): if getattr(self.gui.content_server, 'is_running', False): QTimer.singleShot(50, self.check_exited) return self.gui.content_server = None self.stopping_msg.accept()
def add_url(self): self.items.append(URLItem(None, self)) self.l.addWidget(self.items[-1]) QTimer.singleShot(100, self.scroll_to_bottom)
class GridView(QListView): update_item = pyqtSignal(object) files_dropped = pyqtSignal(object) def __init__(self, parent): QListView.__init__(self, parent) setup_dnd_interface(self) self.setUniformItemSizes(True) self.setWrapping(True) self.setFlow(self.LeftToRight) # We cannot set layout mode to batched, because that breaks # restore_vpos() # self.setLayoutMode(self.Batched) self.setResizeMode(self.Adjust) self.setSelectionMode(self.ExtendedSelection) self.setVerticalScrollMode(self.ScrollPerPixel) self.delegate = CoverDelegate(self) self.delegate.animation.valueChanged.connect( self.animation_value_changed) self.delegate.animation.finished.connect(self.animation_done) self.setItemDelegate(self.delegate) self.setSpacing(self.delegate.spacing) self.padding_left = 0 self.set_color() self.ignore_render_requests = Event() self.thumbnail_cache = ThumbnailCache( max_size=gprefs['cover_grid_disk_cache_size'], thumbnail_size=(self.delegate.cover_size.width(), self.delegate.cover_size.height())) self.render_thread = None self.update_item.connect(self.re_render, type=Qt.QueuedConnection) self.doubleClicked.connect(self.double_clicked) self.setCursor(Qt.PointingHandCursor) self.gui = parent self.context_menu = None self.update_timer = QTimer(self) self.update_timer.setInterval(200) self.update_timer.timeout.connect(self.update_viewport) self.update_timer.setSingleShot(True) self.resize_timer = t = QTimer(self) t.setInterval(200), t.setSingleShot(True) t.timeout.connect(self.update_memory_cover_cache_size) @property def first_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.top(), (self.spacing() * 2) + geom.top(), 5): for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: return ans @property def last_visible_row(self): geom = self.viewport().geometry() for y in xrange(geom.bottom(), geom.bottom() - 2 * self.spacing(), -5): for x in xrange(geom.left(), (self.spacing() * 2) + geom.left(), 5): ans = self.indexAt(QPoint(x, y)).row() if ans > -1: item_width = self.delegate.item_size.width( ) + 2 * self.spacing() return ans + (geom.width() // item_width) def update_viewport(self): self.ignore_render_requests.clear() self.update_timer.stop() m = self.model() for r in xrange(self.first_visible_row or 0, self.last_visible_row or (m.count() - 1)): self.update(m.index(r, 0)) def double_clicked(self, index): d = self.delegate if d.animating is None and not config['disable_animations']: d.animating = index d.animation.start() if tweaks['doubleclick_on_library_view'] == 'open_viewer': self.gui.iactions['View'].view_triggered(index) elif tweaks['doubleclick_on_library_view'] in { 'edit_metadata', 'edit_cell' }: self.gui.iactions['Edit Metadata'].edit_metadata(False, False) def animation_value_changed(self, value): if self.delegate.animating is not None: self.update(self.delegate.animating) def animation_done(self): if self.delegate.animating is not None: idx = self.delegate.animating self.delegate.animating = None self.update(idx) def set_color(self): r, g, b = gprefs['cover_grid_color'] pal = QPalette() col = QColor(r, g, b) pal.setColor(pal.Base, col) tex = gprefs['cover_grid_texture'] if tex: from calibre.gui2.preferences.texture_chooser import texture_path path = texture_path(tex) if path: pm = QPixmap(path) if not pm.isNull(): val = pm.scaled(1, 1).toImage().pixel(0, 0) r, g, b = qRed(val), qGreen(val), qBlue(val) pal.setBrush(pal.Base, QBrush(pm)) dark = (r + g + b) / 3.0 < 128 pal.setColor(pal.Text, QColor(Qt.white if dark else Qt.black)) self.setPalette(pal) self.delegate.highlight_color = pal.color(pal.Text) def refresh_settings(self): size_changed = ( gprefs['cover_grid_width'] != self.delegate.original_width or gprefs['cover_grid_height'] != self.delegate.original_height) if (size_changed or gprefs['cover_grid_show_title'] != self.delegate.original_show_title or gprefs['show_emblems'] != self.delegate.original_show_emblems or gprefs['emblem_size'] != self.delegate.orginal_emblem_size or gprefs['emblem_position'] != self.delegate.orginal_emblem_position): self.delegate.set_dimensions() self.setSpacing(self.delegate.spacing) if size_changed: self.delegate.cover_cache.clear() if gprefs['cover_grid_spacing'] != self.delegate.original_spacing: self.delegate.calculate_spacing() self.setSpacing(self.delegate.spacing) self.set_color() if size_changed: self.thumbnail_cache.set_thumbnail_size( self.delegate.cover_size.width(), self.delegate.cover_size.height()) cs = gprefs['cover_grid_disk_cache_size'] if (cs * (1024**2)) != self.thumbnail_cache.max_size: self.thumbnail_cache.set_size(cs) self.update_memory_cover_cache_size() def resizeEvent(self, ev): self.resize_timer.start() return QListView.resizeEvent(self, ev) def update_memory_cover_cache_size(self): try: sz = self.delegate.item_size except AttributeError: return rows, cols = self.width() // sz.width(), self.height() // sz.height() num = (rows + 1) * (cols + 1) limit = max(100, num * max(2, gprefs['cover_grid_cache_size_multiple'])) if limit != self.delegate.cover_cache.limit: self.delegate.cover_cache.set_limit(limit) def shown(self): self.update_memory_cover_cache_size() if self.render_thread is None: self.thumbnail_cache.set_database(self.gui.current_db) self.render_thread = Thread(target=self.render_covers) self.render_thread.daemon = True self.render_thread.start() def render_covers(self): q = self.delegate.render_queue while True: book_id = q.get() try: if book_id is None: return if self.ignore_render_requests.is_set(): continue try: self.render_cover(book_id) except: import traceback traceback.print_exc() finally: q.task_done() def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id, )) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, self.delegate.cover_size.width(), self.delegate.cover_size.height()) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id, )) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id, )) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id, )) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id) def re_render(self, book_id): self.delegate.cover_cache.clear_staging() m = self.model() try: index = m.db.row(book_id) except (IndexError, ValueError, KeyError): return self.update(m.index(index, 0)) def shutdown(self): self.ignore_render_requests.set() self.delegate.render_queue.put(None) self.thumbnail_cache.shutdown() def set_database(self, newdb, stage=0): if stage == 0: self.ignore_render_requests.set() try: for x in (self.delegate.cover_cache, self.thumbnail_cache): self.model().db.new_api.remove_cover_cache(x) except AttributeError: pass # db is None for x in (self.delegate.cover_cache, self.thumbnail_cache): newdb.new_api.add_cover_cache(x) try: # Use a timeout so that if, for some reason, the render thread # gets stuck, we dont deadlock, future covers wont get # rendered, but this is better than a deadlock join_with_timeout(self.delegate.render_queue) except RuntimeError: print('Cover rendering thread is stuck!') finally: self.ignore_render_requests.clear() else: self.delegate.cover_cache.clear() def select_rows(self, rows): sel = QItemSelection() sm = self.selectionModel() m = self.model() # Create a range based selector for each set of contiguous rows # as supplying selectors for each individual row causes very poor # performance if a large number of rows has to be selected. for k, g in itertools.groupby(enumerate(rows), lambda (i, x): i - x): group = list(map(operator.itemgetter(1), g)) sel.merge( QItemSelection(m.index(min(group), 0), m.index(max(group), 0)), sm.Select) sm.select(sel, sm.ClearAndSelect)
def title_changed(self, title): if self.wait_for_title and title == self.wait_for_title and self.load_complete: QTimer.singleShot(self.settle_time, self.print_to_pdf)
def __init__(self, mi=None, prefs=None, parent=None, for_global_prefs=False): QWidget.__init__(self, parent) self.ignore_changed = False self.for_global_prefs = for_global_prefs self.l = l = QHBoxLayout(self) l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) self.settings_tabs = st = QTabWidget(self) l.addWidget(st) self.preview_label = la = Preview(self) l.addWidget(la) if prefs is None: prefs = cprefs self.original_prefs = prefs self.mi = mi or self.default_mi() self.colors_page = cp = QWidget(st) st.addTab(cp, _('&Colors')) cp.l = l = QGridLayout() cp.setLayout(l) if for_global_prefs: msg = _('When generating covers, a color scheme for the cover is chosen at random from the' ' color schemes below. You can prevent an individual scheme from being selected by' ' unchecking it. The preview on the right shows the currently selected color scheme.') else: msg = _('Choose a color scheme to be used for this generated cover.') + '<p>' + _( 'In normal cover generation, the color scheme is chosen at random from the list of color schemes below. You' ' can prevent an individual color scheme from being chosen by unchecking it here.') cp.la = la = QLabel('<p>' + msg) la.setWordWrap(True) l.addWidget(la, 0, 0, 1, -1) self.colors_list = cl = QListWidget(cp) l.addWidget(cl, 1, 0, 1, -1) self.colors_map = OrderedDict() self.ncs = ncs = QPushButton(QIcon(I('plus.png')), _('&New color scheme'), cp) ncs.clicked.connect(self.create_color_scheme) l.addWidget(ncs) self.ecs = ecs = QPushButton(QIcon(I('format-fill-color.png')), _('&Edit color scheme'), cp) ecs.clicked.connect(self.edit_color_scheme) l.addWidget(ecs, l.rowCount()-1, 1) self.rcs = rcs = QPushButton(QIcon(I('minus.png')), _('&Remove color scheme'), cp) rcs.clicked.connect(self.remove_color_scheme) l.addWidget(rcs, l.rowCount()-1, 2) self.styles_page = sp = QWidget(st) st.addTab(sp, _('&Styles')) sp.l = l = QVBoxLayout() sp.setLayout(l) if for_global_prefs: msg = _('When generating covers, a style for the cover is chosen at random from the' ' styles below. You can prevent an individual style from being selected by' ' unchecking it. The preview on the right shows the currently selected style.') else: msg = _('Choose a style to be used for this generated cover.') + '<p>' + _( 'In normal cover generation, the style is chosen at random from the list of styles below. You' ' can prevent an individual style from being chosen by unchecking it here.') sp.la = la = QLabel('<p>' + msg) la.setWordWrap(True) l.addWidget(la) self.styles_list = sl = QListWidget(sp) l.addWidget(sl) self.style_map = OrderedDict() self.font_page = fp = QWidget(st) st.addTab(fp, _('&Fonts and sizes')) fp.l = l = QFormLayout() fp.setLayout(l) fp.f = [] def add_hline(): f = QFrame() fp.f.append(f) f.setFrameShape(f.HLine) l.addRow(f) for x, label, size_label in ( ('title', _('&Title font family:'), _('&Title font size:')), ('subtitle', _('&Subtitle font family'), _('&Subtitle font size:')), ('footer', _('&Footer font family'), _('&Footer font size')), ): attr = '%s_font_family' % x ff = FontFamilyChooser(fp) setattr(self, attr, ff) l.addRow(label, ff) ff.family_changed.connect(self.emit_changed) attr = '%s_font_size' % x fs = QSpinBox(fp) setattr(self, attr, fs) fs.setMinimum(8), fs.setMaximum(200), fs.setSuffix(' px') fs.setValue(prefs[attr]) fs.valueChanged.connect(self.emit_changed) l.addRow(size_label, fs) add_hline() self.changed_timer = t = QTimer(self) t.setSingleShot(True), t.setInterval(500), t.timeout.connect(self.emit_changed) def create_sz(label): ans = QSpinBox(self) ans.setSuffix(' px'), ans.setMinimum(100), ans.setMaximum(10000) l.addRow(label, ans) ans.valueChanged.connect(self.changed_timer.start) return ans self.cover_width = create_sz(_('Cover &width:')) self.cover_height = create_sz(_('Cover &height:')) fp.cla = la = QLabel(_( 'Note that the preview to the side is of fixed aspect ratio, so changing the cover' ' width above will not have any effect. If you change the height, you should also change the width nevertheless' ' as it will be used in actual cover generation.')) la.setWordWrap(True) l.addRow(la) self.templates_page = tp = QWidget(st) st.addTab(tp, _('&Text')) tp.l = l = QVBoxLayout() tp.setLayout(l) tp.la = la = QLabel(_( 'The text on the generated cover is taken from the metadata of the book.' ' This is controlled via templates. You can use the <b>, <i> and <br> tags' ' in the templates for bold, italic and line breaks, respectively. The' ' default templates use the title, series and authors. You can change them to use' ' whatever metadata you like.')) la.setWordWrap(True), la.setTextFormat(Qt.PlainText) l.addWidget(la) def create_template_widget(title, which, button): attr = which + '_template' heading = QLabel('<h2>' + title) setattr(tp, attr + '_heading', heading) l.addWidget(heading) la = QLabel() setattr(self, attr, la) l.addWidget(la), la.setTextFormat(Qt.PlainText), la.setStyleSheet('QLabel {font-family: monospace}') la.setWordWrap(True) b = QPushButton(button) b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) connect_lambda(b.clicked, self, lambda self: self.change_template(which)) setattr(self, attr + '_button', b) l.addWidget(b) if which != 'footer': f = QFrame(tp) setattr(tp, attr + '_sep', f), f.setFrameShape(QFrame.HLine) l.addWidget(f) l.addSpacing(10) create_template_widget(_('The title template'), 'title', _('Change the &title template')) create_template_widget(_('The sub-title template'), 'subtitle', _('Change the &sub-title template')) create_template_widget(_('The footer template'), 'footer', _('Change the &footer template')) l.addStretch(2) self.apply_prefs(prefs) self.changed.connect(self.update_preview) self.styles_list.itemSelectionChanged.connect(self.update_preview) self.colors_list.itemSelectionChanged.connect(self.update_preview) self.update_preview()
def start(self): self.restorer.start() QTimer.singleShot(10, self.update)
class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) def __init__(self, preview, parent=None): QWidget.__init__(self, parent) self.preview = preview self.preview_is_refreshing = False self.refresh_needed = False preview.refresh_starting.connect(self.preview_refresh_starting) preview.refreshed.connect(self.preview_refreshed) self.apply_theme() self.setAutoFillBackground(True) self.update_timer = QTimer(self) self.update_timer.timeout.connect(self.update_data) self.update_timer.setSingleShot(True) self.update_timer.setInterval(500) self.now_showing = (None, None, None) self.stack = s = QStackedLayout(self) self.setLayout(s) self.clear_label = la = QLabel( '<h3>' + _('No style information found') + '</h3><p>' + _('Move the cursor inside a HTML tag to see what styles' ' apply to that tag.')) la.setWordWrap(True) la.setAlignment(Qt.AlignTop | Qt.AlignLeft) s.addWidget(la) self.box = box = Box(self) box.hyperlink_activated.connect(self.goto_declaration, type=Qt.QueuedConnection) self.scroll = sc = QScrollArea(self) sc.setWidget(box) sc.setWidgetResizable(True) s.addWidget(sc) def preview_refresh_starting(self): self.preview_is_refreshing = True def preview_refreshed(self): self.preview_is_refreshing = False # We must let the event loop run otherwise the webview will return # stale data in read_data() self.refresh_needed = True self.start_update_timer() def apply_theme(self): f = self.font() f.setFamily(tprefs['editor_font_family'] or default_font_family()) f.setPointSize(tprefs['editor_font_size']) self.setFont(f) theme = get_theme(tprefs['editor_theme']) pal = self.palette() pal.setColor(pal.Window, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.WindowText, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'HighlightRegion', 'bg')) pal.setColor(pal.Link, theme_color(theme, 'Link', 'fg')) pal.setColor(pal.LinkVisited, theme_color(theme, 'Keyword', 'fg')) self.setPalette(pal) if hasattr(self, 'box'): self.box.relayout() self.update() def clear(self): self.stack.setCurrentIndex(0) def show_data(self, editor_name, sourceline, tags): if self.preview_is_refreshing: return if sourceline is None: self.clear() else: data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: if editor_name == self.current_name and ( editor_name, sourceline, tags) == self.now_showing: # Try again in a little while in case there was a transient # error in the web view self.start_update_timer() return if self.now_showing == ( None, None, None) or self.now_showing[0] != self.current_name: self.clear() return # Try to refresh the data for the currently shown tag instead # of clearing editor_name, sourceline, tags = self.now_showing data = self.read_data(sourceline, tags) if data is None or len(data['computed_css']) < 1: self.clear() return self.now_showing = (editor_name, sourceline, tags) data['html_name'] = editor_name self.box.show_data(data) self.refresh_needed = False self.stack.setCurrentIndex(1) def read_data(self, sourceline, tags): mf = self.preview.view.page().mainFrame() tags = [x.lower() for x in tags] result = unicode( mf.evaluateJavaScript( 'window.calibre_preview_integration.live_css(%s, %s)' % (json.dumps(sourceline), json.dumps(tags))) or '') try: result = json.loads(result) except ValueError: result = None if result is not None: maximum_specificities = {} for node in result['nodes']: is_ancestor = node['is_ancestor'] for rule in node['css']: self.process_rule(rule, is_ancestor, maximum_specificities) for node in result['nodes']: for rule in node['css']: for prop in rule['properties']: if prop.specificity < maximum_specificities[prop.name]: prop.is_overriden = True return result def process_rule(self, rule, is_ancestor, maximum_specificities): selector = rule['selector'] sheet_index = rule['sheet_index'] rule_address = rule['rule_address'] or () if selector is not None: try: specificity = [0] + list(parse(selector)[0].specificity()) except (AttributeError, TypeError): specificity = [0, 0, 0, 0] else: # style attribute specificity = [1, 0, 0, 0] specificity.extend((sheet_index, tuple(rule_address))) ancestor_specificity = 0 if is_ancestor else 1 properties = [] for prop in rule['properties']: important = 1 if prop[-1] == 'important' else 0 p = Property(prop, [ancestor_specificity] + [important] + specificity) properties.append(p) if p.specificity > maximum_specificities.get( p.name, (0, 0, 0, 0, 0, 0)): maximum_specificities[p.name] = p.specificity rule['properties'] = properties href = rule['href'] if hasattr(href, 'startswith') and href.startswith('file://'): href = href[len('file://'):] if iswindows and href.startswith('/'): href = href[1:] if href: rule['href'] = current_container().abspath_to_name( href, root=self.preview.current_root) @property def current_name(self): return self.preview.current_name @property def is_visible(self): return self.isVisible() def showEvent(self, ev): self.update_timer.start() actions['auto-reload-preview'].setEnabled(True) return QWidget.showEvent(self, ev) def sync_to_editor(self): self.update_data() def update_data(self): if not self.is_visible or self.preview_is_refreshing: return editor_name = self.current_name ed = editors.get(editor_name, None) if self.update_timer.isActive() or (ed is None and editor_name is not None): return QTimer.singleShot(100, self.update_data) if ed is not None: sourceline, tags = ed.current_tag(for_position_sync=False) if self.refresh_needed or self.now_showing != (editor_name, sourceline, tags): self.show_data(editor_name, sourceline, tags) def start_update_timer(self): if self.is_visible: self.update_timer.start() def stop_update_timer(self): self.update_timer.stop() def navigate_to_declaration(self, data, editor): if data['type'] == 'inline': sourceline, tags = data['sourceline_address'] editor.goto_sourceline(sourceline, tags, attribute='style') elif data['type'] == 'sheet': editor.goto_css_rule(data['rule_address']) elif data['type'] == 'elem': editor.goto_css_rule(data['rule_address'], sourceline_address=data['sourceline_address'])