Example #1
2
    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)
Example #2
1
    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
Example #3
0
 def check(self):
     if self.rejected:
         return
     if self.thread.is_alive():
         QTimer.singleShot(100, self.check)
     else:
         self.accept()
Example #4
0
 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)
Example #5
0
 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
Example #6
0
    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')
Example #7
0
 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)
Example #8
0
    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)
Example #9
0
 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_()
Example #10
0
    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')
Example #11
0
    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)
Example #12
0
    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)
Example #13
0
    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)
Example #14
0
    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
Example #15
0
    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)
Example #16
0
 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)
Example #17
0
 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
Example #18
0
    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)
Example #19
0
    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()
Example #20
0
 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
            )
Example #22
0
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_()
Example #23
0
File: jobs.py Project: kba/calibre
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'))
Example #24
0
 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)
Example #25
0
 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)
Example #26
0
    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()
Example #27
0
 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()
Example #28
0
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)
Example #29
0
    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))
Example #30
0
 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())
Example #31
0
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())
Example #32
0
 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))
Example #33
0
    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)
Example #34
0
 def show_frag(self):
     self.view.show_frag(self.current_frag)
     QTimer.singleShot(1, self.check_frag)
Example #35
0
 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)
Example #36
0
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)
Example #37
0
 def start(self):
     t = self.thread = Thread(target=self.vacuum)
     t.daemon = True
     t.start()
     QTimer.singleShot(100, self.check)
     self.exec_()
Example #38
0
 def cover_view_resized(self, event):
     QTimer.singleShot(1, self.resize_cover)
Example #39
0
 def update(self):
     if self.worker.is_alive():
         QTimer.singleShot(50, self.update)
     else:
         self.process_results()
Example #40
0
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)
Example #43
0
    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
Example #44
0
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
Example #45
0
        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_()
Example #46
0
    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)
Example #47
0
 def display(self):
     if not self.isVisible():
         self.show()
     self.raise_()
     QTimer.singleShot(0, self.model.build)
Example #48
0
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)
Example #49
0
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
Example #50
0
    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()
Example #51
0
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("用例运行完毕,邮件已发送")
Example #52
0
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)
Example #53
0
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)
Example #54
0
 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()
Example #55
0
 def add_url(self):
     self.items.append(URLItem(None, self))
     self.l.addWidget(self.items[-1])
     QTimer.singleShot(100, self.scroll_to_bottom)
Example #56
0
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)
Example #57
0
 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)
Example #58
0
    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()
Example #59
0
 def start(self):
     self.restorer.start()
     QTimer.singleShot(10, self.update)
Example #60
0
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'])