From 4d27e7fa9d32f4e05973395c32fb0f1eab5f6a47 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Apr 2011 13:12:23 +0100 Subject: [PATCH 1/7] Fix 753122: in preferences, clear a content server restriction if the saved search no longer exists. --- src/calibre/gui2/preferences/server.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index 421dbe737f..f4a00c0932 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -18,6 +18,7 @@ from calibre.utils.config import ConfigProxy from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ Dispatcher, info_dialog from calibre import as_unicode +from calibre.utils.icu import sort_key class ConfigWidget(ConfigWidgetBase, Ui_Form): @@ -42,8 +43,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): else self.opt_password.Password)) self.opt_password.setEchoMode(self.opt_password.Password) - restrictions = sorted(saved_searches().names(), - cmp=lambda x,y: cmp(x.lower(), y.lower())) + restrictions = sorted(saved_searches().names(), key=sort_key) + # verify that the current restriction still exists. If not, clear it. + csr = db.prefs.get('cs_restriction', None) + if csr and csr not in restrictions: + db.prefs.set('cs_restriction', '') choices = [('', '')] + [(x, x) for x in restrictions] r('cs_restriction', db.prefs, choices=choices) From 0ba4f9a8adc6f9b2e4b77aa701a214c890f4fb99 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 09:33:47 -0600 Subject: [PATCH 2/7] ... --- recipes/economist.recipe | 3 ++- recipes/economist_free.recipe | 3 ++- recipes/financial_times.recipe | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/recipes/economist.recipe b/recipes/economist.recipe index 9447fe2193..894f5880b3 100644 --- a/recipes/economist.recipe +++ b/recipes/economist.recipe @@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe): __author__ = "Kovid Goyal" INDEX = 'http://www.economist.com/printedition' - description = 'Global news and current affairs from a European perspective.' + description = ('Global news and current affairs from a European' + ' perspective. Best downloaded on Friday mornings (GMT)') oldest_article = 7.0 cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' diff --git a/recipes/economist_free.recipe b/recipes/economist_free.recipe index d1766211d7..4f060dc487 100644 --- a/recipes/economist_free.recipe +++ b/recipes/economist_free.recipe @@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe): language = 'en' __author__ = "Kovid Goyal" - description = ('Global news and current affairs from a European perspective.' + description = ('Global news and current affairs from a European' + ' perspective. Best downloaded on Friday mornings (GMT).' ' Much slower than the print edition based version.') oldest_article = 7.0 diff --git a/recipes/financial_times.recipe b/recipes/financial_times.recipe index 25efc56e45..0e3c91d3e3 100644 --- a/recipes/financial_times.recipe +++ b/recipes/financial_times.recipe @@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe class FinancialTimes(BasicNewsRecipe): title = u'Financial Times' __author__ = 'Darko Miletic and Sujata Raman' - description = 'Financial world news' + description = ('Financial world news. Available after 5AM ' + 'GMT, daily.') oldest_article = 2 language = 'en' From ebe5b28567e467df846ca15bb8c31123b5106026 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 11:01:08 -0600 Subject: [PATCH 3/7] ... --- .../ebooks/metadata/sources/identify.py | 8 +-- src/calibre/gui2/metadata/single_download.py | 64 +++++++++++++++---- src/calibre/utils/logging.py | 24 +++++++ 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 8c6172f0e2..075c780596 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -253,10 +253,10 @@ def identify(log, abort, # {{{ plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()] kwargs = { - 'title': title, - 'authors': authors, - 'identifiers': identifiers, - 'timeout': timeout, + 'title': title, + 'authors': authors, + 'identifiers': identifiers, + 'timeout': timeout, } log('Running identify query with parameters:') diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 049ac611c5..19e92adf4f 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -11,23 +11,16 @@ from threading import Thread, Event from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, - QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette) + QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, + QTimer, pyqtSignal) from PyQt4.QtWebKit import QWebView from calibre.customize.ui import metadata_plugins from calibre.ebooks.metadata import authors_to_string -from calibre.utils.logging import ThreadSafeLog, UnicodeHTMLStream +from calibre.utils.logging import GUILog as Log from calibre.ebooks.metadata.sources.identify import identify +from calibre.gui2 import error_dialog -class Log(ThreadSafeLog): # {{{ - - def __init__(self): - ThreadSafeLog.__init__(self, level=self.DEBUG) - self.outputs = [UnicodeHTMLStream()] - - def clear(self): - self.outputs[0].clear() -# }}} class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -56,10 +49,11 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class ResultsView(QTableView): +class ResultsView(QTableView): # {{{ def __init__(self, parent=None): QTableView.__init__(self, parent) +# }}} class Comments(QWebView): # {{{ @@ -109,7 +103,7 @@ class Comments(QWebView): # {{{ self.setHtml(templ%html) # }}} -class IdentifyWorker(Thread): +class IdentifyWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers): Thread.__init__(self) @@ -131,8 +125,11 @@ class IdentifyWorker(Thread): except: import traceback self.error = traceback.format_exc() +# }}} -class IdentifyWidget(QWidget): +class IdentifyWidget(QWidget): # {{{ + + rejected = pyqtSignal() def __init__(self, log, parent=None): QWidget.__init__(self, parent) @@ -199,6 +196,40 @@ class IdentifyWidget(QWidget): # self.worker.start() + QTimer.singleShot(50, self.update) + + def update(self): + if self.worker.is_alive(): + QTimer.singleShot(50, self.update) + return + self.process_results() + + def process_results(self): + if self.worker.error is not None: + error_dialog(self, _('Download failed'), + _('Failed to download metadata. Click ' + 'Show Details to see details'), + show=True, det_msg=self.wroker.error) + self.rejected.emit() + return + + if not self.worker.results: + log = ''.join(self.log.plain_text) + error_dialog(self, _('No matches found'), '

' + + _('Failed to find any books that ' + 'match your search. Try making the search less ' + 'specific. For example, use only the author\'s ' + 'last name and a single distinctive word from ' + 'the title.

To see the full log, click Show Details.'), + show=True, det_msg=log) + self.rejected.emit() + return + + + def cancel(self): + self.abort.set() +# }}} + class FullFetch(QDialog): # {{{ def __init__(self, log, parent=None): @@ -218,6 +249,7 @@ class FullFetch(QDialog): # {{{ self.bb.rejected.connect(self.reject) self.identify_widget = IdentifyWidget(log, self) + self.identify_widget.rejected.connect(self.reject) self.stack.addWidget(self.identify_widget) self.resize(850, 500) @@ -225,6 +257,10 @@ class FullFetch(QDialog): # {{{ # Prevent pressing Enter from closing the dialog pass + def reject(self): + self.identify_widget.cancel() + return QDialog.reject(self) + def start(self, title=None, authors=None, identifiers={}): self.identify_widget.start(title=title, authors=authors, identifiers=identifiers) diff --git a/src/calibre/utils/logging.py b/src/calibre/utils/logging.py index 45e21ded39..dbbca6806b 100644 --- a/src/calibre/utils/logging.py +++ b/src/calibre/utils/logging.py @@ -108,10 +108,13 @@ class UnicodeHTMLStream(HTMLStream): elif not isinstance(arg, unicode): arg = as_unicode(arg) self.data.append(arg+sep) + self.plain_text.append(arg+sep) self.data.append(end) + self.plain_text.append(end) def clear(self): self.data = [] + self.plain_text = [] self.last_col = self.color[INFO] @property @@ -162,4 +165,25 @@ class ThreadSafeLog(Log): with self._lock: Log.prints(self, *args, **kwargs) +class GUILog(ThreadSafeLog): + + ''' + Logs in HTML and plain text as unicode. Ideal for display in a GUI context. + ''' + + def __init__(self): + ThreadSafeLog.__init__(self, level=self.DEBUG) + self.outputs = [UnicodeHTMLStream()] + + def clear(self): + self.outputs[0].clear() + + @property + def html(self): + return self.outputs[0].html + + @property + def plain_text(self): + return u''.join(self.outputs[0].plain_text) + default_log = Log() From 903f628f70045d5bdb41b6ab19a0e7bdee29d8da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 11:59:47 -0600 Subject: [PATCH 4/7] ... --- .../ebooks/metadata/sources/identify.py | 4 + src/calibre/gui2/metadata/single_download.py | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 075c780596..85549904e7 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -217,6 +217,10 @@ class ISBNMerge(object): for r in results: ans.identifiers.update(r.identifiers) + # Cover URL + ans.has_cached_cover_url = bool([r for r in results if + getattr(r, 'has_cached_cover_url', False)]) + # Merge any other fields with no special handling (random merge) touched_fields = set() for r in results: diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 19e92adf4f..94ee8f8a13 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -12,14 +12,16 @@ from threading import Thread, Event from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, - QTimer, pyqtSignal) + QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize) from PyQt4.QtWebKit import QWebView from calibre.customize.ui import metadata_plugins from calibre.ebooks.metadata import authors_to_string from calibre.utils.logging import GUILog as Log from calibre.ebooks.metadata.sources.identify import identify -from calibre.gui2 import error_dialog +from calibre.ebooks.metadata.book.base import Metadata +from calibre.gui2 import error_dialog, NONE +from calibre.utils.date import utcnow, fromordinal, format_date class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -49,10 +51,85 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} +class ResultsModel(QAbstractTableModel): + + COLUMNS = ( + '#', _('Title'), _('Published'), _('Has cover'), _('Has summary') + ) + HTML_COLS = (1, 2) + ICON_COLS = (3, 4) + + def __init__(self, results, parent=None): + QAbstractTableModel.__init__(self, parent) + self.results = results + self.yes_icon = QVariant(QIcon(I('ok.png'))) + + def rowCount(self, parent=None): + return len(self.results) + + def columnCount(self, parent=None): + return len(self.COLUMNS) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + if orientation == Qt.Horizontal: + try: + return QVariant(self.COLUMNS[section]) + except: + return NONE + else: + return QVariant(unicode(section+1)) + + def data_as_text(self, book, col): + if col == 0: + return unicode(book.gui_rank+1) + if col == 1: + t = book.title if book.title else _('Unknown') + a = authors_to_string(book.authors) if book.authors else '' + return '%s
%s' % (t, a) + if col == 2: + d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown') + p = book.publisher if book.publisher else '' + return '%s
%s' % (d, p) + + + def data(self, index, role): + row, col = index.row(), index.column() + try: + book = self.results[row] + except: + return NONE + if role == Qt.DisplayRole and col not in self.ICON_COLS: + res = self.data_as_text(book, col) + if res: + return QVariant(res) + return NONE + elif role == Qt.DecorationRole and col in self.ICON_COLS: + if col == 3 and getattr(book, 'has_cached_cover_url', False): + return self.yes_icon + if col == 4 and book.comments: + return self.yes_icon + return NONE + class ResultsView(QTableView): # {{{ def __init__(self, parent=None): QTableView.__init__(self, parent) + self.rt_delegate = RichTextDelegate(self) + self.setSelectionMode(self.SingleSelection) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(self.SelectRows) + self.setIconSize(QSize(24, 24)) + + def show_results(self, results): + self._model = ResultsModel(results, self) + self.setModel(self._model) + for i in self._model.HTML_COLS: + self.setItemDelegateForColumn(i, self.rt_delegate) + self.resizeRowsToContents() + self.resizeColumnsToContents() + # }}} class Comments(QWebView): # {{{ @@ -60,8 +137,8 @@ class Comments(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) self.setAcceptDrops(False) - self.setMaximumWidth(270) - self.setMinimumWidth(270) + self.setMaximumWidth(300) + self.setMinimumWidth(300) palette = self.palette() palette.setBrush(QPalette.Base, Qt.transparent) @@ -116,10 +193,30 @@ class IdentifyWorker(Thread): # {{{ self.results = [] self.error = None + def sample_results(self): + m1 = Metadata('The Great Gatsby', ['Francis Scott Fitzgerald']) + m2 = Metadata('The Great Gatsby', ['F. Scott Fitzgerald']) + m1.has_cached_cover_url = True + m2.has_cached_cover_url = False + m1.comments = 'Some comments '*10 + m1.tags = ['tag%d'%i for i in range(20)] + m1.rating = 4.4 + m1.language = 'en' + m2.language = 'fr' + m1.pubdate = utcnow() + m2.pubdate = fromordinal(1000000) + m1.publisher = 'Publisher 1' + m2.publisher = 'Publisher 2' + + return [m1, m2] + def run(self): try: - self.results = identify(self.log, self.abort, title=self.title, - authors=self.authors, identifiers=self.identifiers) + if True: + self.results = self.sample_results() + else: + self.results = identify(self.log, self.abort, title=self.title, + authors=self.authors, identifiers=self.identifiers) for i, result in enumerate(self.results): result.gui_rank = i except: @@ -194,22 +291,22 @@ class IdentifyWidget(QWidget): # {{{ self.worker = IdentifyWorker(self.log, self.abort, title, authors, identifiers) - # self.worker.start() + self.worker.start() QTimer.singleShot(50, self.update) def update(self): if self.worker.is_alive(): QTimer.singleShot(50, self.update) - return - self.process_results() + else: + self.process_results() def process_results(self): if self.worker.error is not None: error_dialog(self, _('Download failed'), _('Failed to download metadata. Click ' 'Show Details to see details'), - show=True, det_msg=self.wroker.error) + show=True, det_msg=self.worker.error) self.rejected.emit() return @@ -225,6 +322,8 @@ class IdentifyWidget(QWidget): # {{{ self.rejected.emit() return + self.results_view.show_results(self.worker.results) + def cancel(self): self.abort.set() From f1210c3444f29acb175237b080eb0696f5562b6f Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 7 Apr 2011 18:17:28 -0400 Subject: [PATCH 5/7] Disable toolbar icons, forces icon text to be displayed when enabled. --- src/calibre/gui2/layout.py | 4 ++-- src/calibre/gui2/preferences/look_feel.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 41b415e25c..c73bb0d0fa 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -264,11 +264,11 @@ class ToolBar(QToolBar): # {{{ def apply_settings(self): sz = gprefs['toolbar_icon_size'] - sz = {'small':24, 'medium':48, 'large':64}[sz] + sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz] self.setIconSize(QSize(sz, sz)) self.child_bar.setIconSize(QSize(sz, sz)) style = Qt.ToolButtonTextUnderIcon - if gprefs['toolbar_text'] == 'never': + if sz > 0 and gprefs['toolbar_text'] == 'never': style = Qt.ToolButtonIconOnly self.setToolButtonStyle(style) self.child_bar.setToolButtonStyle(style) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 523a296a37..71b9e38667 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('use_roman_numerals_for_series_number', config) r('separate_cover_flow', config, restart_required=True) - choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), - (_('Large'), 'large')] + choices = [(_('Off'), 'off'), (_('Small'), 'small'), + (_('Medium'), 'medium'), (_('Large'), 'large')] r('toolbar_icon_size', gprefs, choices=choices) choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'), From 7fe19d6774c4c00ae86592285537aa5a0777c9e9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 18:04:22 -0600 Subject: [PATCH 6/7] Fix #754154 (Option to change default Library button behaviour) --- src/calibre/gui2/actions/__init__.py | 1 - src/calibre/gui2/actions/choose_library.py | 7 ++++++- src/calibre/gui2/layout.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index c0dd40326d..589e14cff2 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -145,7 +145,6 @@ class InterfaceAction(QObject): ans[candidate] = zf.read(candidate) return ans - def genesis(self): ''' Setup this plugin. Only called once during initialization. self.gui is diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 6f4ca624cb..fd7959a30e 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en' import os, shutil from functools import partial -from PyQt4.Qt import QMenu, Qt, QInputDialog +from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton from calibre import isbytestring from calibre.constants import filesystem_encoding @@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction): type=Qt.QueuedConnection) self.stats = LibraryUsageStats() + self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else + QToolButton.MenuButtonPopup) + self.create_action(spec=(_('Switch/create library...'), 'lt.png', None, None), attr='action_choose') self.action_choose.triggered.connect(self.choose_library, @@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction): type=Qt.QueuedConnection) self.choose_menu.addAction(ac) + self.rename_separator = self.choose_menu.addSeparator() self.maintenance_menu = QMenu(_('Library Maintenance')) @@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction): return db = self.gui.library_view.model().db locations = list(self.stats.locations(db)) + for ac in self.switch_actions: ac.setVisible(False) self.quick_menu.clear() diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c73bb0d0fa..29c2cd4a7a 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en' from functools import partial -from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \ - pyqtSignal, QToolButton, QMenu, \ - QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup +from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize, + pyqtSignal, QToolButton, QMenu, + QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup) from calibre.constants import __appname__ From 638f70b640c605ae3661c1760f14347399e44fb4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 18:09:39 -0600 Subject: [PATCH 7/7] ... --- src/calibre/gui2/actions/__init__.py | 2 +- src/calibre/gui2/metadata/single_download.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index 589e14cff2..a68659864a 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -148,7 +148,7 @@ class InterfaceAction(QObject): def genesis(self): ''' Setup this plugin. Only called once during initialization. self.gui is - available. The action secified by :attr:`action_spec` is available as + available. The action specified by :attr:`action_spec` is available as ``self.qaction``. ''' pass diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 94ee8f8a13..eb067e1211 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -71,15 +71,12 @@ class ResultsModel(QAbstractTableModel): return len(self.COLUMNS) def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: - return NONE - if orientation == Qt.Horizontal: + if orientation == Qt.Horizontal and role == Qt.DisplayRole: try: return QVariant(self.COLUMNS[section]) except: return NONE - else: - return QVariant(unicode(section+1)) + return NONE def data_as_text(self, book, col): if col == 0: @@ -324,6 +321,11 @@ class IdentifyWidget(QWidget): # {{{ self.results_view.show_results(self.worker.results) + self.comments_view.show_data(''' +

Found %d results
+
To see details, click on any result
''' % + len(self.worker.results)) + def cancel(self): self.abort.set()