diff --git a/recipes/al_ahram.recipe b/recipes/al_ahram.recipe new file mode 100644 index 0000000000..8850e82ebf --- /dev/null +++ b/recipes/al_ahram.recipe @@ -0,0 +1,62 @@ +# coding=utf-8 +__license__ = 'GPL v3' +__copyright__ = '2011, Hassan Williamson ' +''' +ahram.org.eg +''' +from calibre.web.feeds.recipes import BasicNewsRecipe + +class AlAhram(BasicNewsRecipe): + title = 'Al-Ahram' + __author__ = 'Hassan Williamson' + description = 'News from Egypt in Arabic.' + oldest_article = 7 + max_articles_per_feed = 100 + no_stylesheets = True + #delay = 1 + use_embedded_content = False + encoding = 'utf8' + publisher = 'Al-Ahram' + category = 'News' + language = 'ar' + publication_type = 'newsportal' + extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif; direction: rtl; } .txtTitle{ font-weight: bold; } ' + + + keep_only_tags = [ + dict(name='div', attrs={'class':['bbcolright']}) + ] + + remove_tags = [ + dict(name='div', attrs={'class':['bbnav', 'bbsp']}), + dict(name='div', attrs={'id':['AddThisButton']}) + ] + + remove_attributes = [ + 'width','height' + ] + + feeds = [ + (u'الأولى', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=25'), + (u'مصر', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=27'), + (u'المحافظات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=29'), + (u'الوطن العربي', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=31'), + (u'العالم', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=26'), + (u'تقارير المراسلين', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=2'), + (u'تحقيقات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=3'), + (u'قضايا واراء', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=4'), + (u'اقتصاد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=5'), + (u'رياضة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=6'), + (u'حوادث', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=38'), + (u'دنيا الثقافة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=7'), + (u'المراة والطفل', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=8'), + (u'يوم جديد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=9'), + (u'الكتاب', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=10'), + (u'الاعمدة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=11'), + (u'أراء حرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=59'), + (u'ملفات الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=12'), + (u'بريد الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=15'), + (u'الاخيرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=16'), + ] + + 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' diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 8c6172f0e2..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: @@ -253,10 +257,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/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 42974be355..634f7f5fce 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -97,6 +97,10 @@ class CSSSelector(etree.XPath): def __init__(self, css, namespaces=XPNSMAP): css = self.MIN_SPACE_RE.sub(r'\1', css) + if isinstance(css, unicode): + # Workaround for bug in lxml on windows/OS X that causes a massive + # memory leak with non ASCII selectors + css = css.encode('ascii', 'ignore').decode('ascii') try: path = css_to_xpath(css) except UnicodeEncodeError: # Bug in css_to_xpath diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index c0dd40326d..a68659864a 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -145,11 +145,10 @@ class InterfaceAction(QObject): ans[candidate] = zf.read(candidate) return ans - 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/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/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index c3ceb27e7e..e29e6c344d 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -141,15 +141,18 @@ class EditMetadataAction(InterfaceAction): list(range(self.gui.library_view.model().rowCount(QModelIndex()))) current_row = row_list.index(cr) - if test_eight_code: - changed = self.do_edit_metadata(row_list, current_row) - else: - changed = self.do_edit_metadata_old(row_list, current_row) + func = (self.do_edit_metadata if test_eight_code else + self.do_edit_metadata_old) + changed, rows_to_refresh = func(row_list, current_row) + + m = self.gui.library_view.model() + + if rows_to_refresh: + m.refresh_rows(rows_to_refresh) if changed: - self.gui.library_view.model().refresh_ids(list(changed)) + m.refresh_ids(list(changed)) current = self.gui.library_view.currentIndex() - m = self.gui.library_view.model() if self.gui.cover_flow: self.gui.cover_flow.dataChanged() m.current_changed(current, previous) @@ -183,6 +186,7 @@ class EditMetadataAction(InterfaceAction): current_row += d.row_delta self.gui.library_view.set_current_row(current_row) self.gui.library_view.scroll_to_row(current_row) + return changed, set() def do_edit_metadata(self, row_list, current_row): from calibre.gui2.metadata.single import edit_metadata @@ -190,7 +194,7 @@ class EditMetadataAction(InterfaceAction): changed, rows_to_refresh = edit_metadata(db, row_list, current_row, parent=self.gui, view_slot=self.view_format_callback, set_current_callback=self.set_current_callback) - return changed + return changed, rows_to_refresh def set_current_callback(self, id_): db = self.gui.library_view.model().db diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 41b415e25c..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__ @@ -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/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 049ac611c5..176c164d3d 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -8,26 +8,22 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' from threading import Thread, Event +from operator import attrgetter 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, 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 ThreadSafeLog, UnicodeHTMLStream +from calibre.utils.logging import GUILog as Log from calibre.ebooks.metadata.sources.identify import identify - -class Log(ThreadSafeLog): # {{{ - - def __init__(self): - ThreadSafeLog.__init__(self, level=self.DEBUG) - self.outputs = [UnicodeHTMLStream()] - - def clear(self): - self.outputs[0].clear() -# }}} +from calibre.ebooks.metadata.book.base import Metadata +from calibre.gui2 import error_dialog, NONE +from calibre.utils.date import utcnow, fromordinal, format_date +from calibre.library.comments import comments_to_html class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -56,18 +52,149 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class ResultsView(QTableView): +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 orientation == Qt.Horizontal and role == Qt.DisplayRole: + try: + return QVariant(self.COLUMNS[section]) + except: + return NONE + return NONE + + 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 + elif role == Qt.UserRole: + return book + return NONE + + def sort(self, col, order=Qt.AscendingOrder): + key = lambda x: x + if col == 0: + key = attrgetter('gui_rank') + elif col == 1: + key = attrgetter('title') + elif col == 2: + key = attrgetter('authors') + elif col == 3: + key = attrgetter('has_cached_cover_url') + elif key == 4: + key = lambda x: bool(x.comments) + + self.results.sort(key=key, reverse=order==Qt.AscendingOrder) + self.reset() + +# }}} + +class ResultsView(QTableView): # {{{ + + show_details_signal = pyqtSignal(object) + book_selected = pyqtSignal(object) 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)) + self.clicked.connect(self.show_details) + self.doubleClicked.connect(self.select_index) + self.setSortingEnabled(True) + + 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() + self.setFocus(Qt.OtherFocusReason) + + def currentChanged(self, current, previous): + ret = QTableView.currentChanged(self, current, previous) + self.show_details(current) + return ret + + def show_details(self, index): + book = self.model().data(index, Qt.UserRole) + parts = [ + '
', + '

%s

'%book.title, + '
%s
'%authors_to_string(book.authors), + ] + if not book.is_null('rating'): + parts.append('
%s
'%('\u2605'*int(book.rating))) + parts.append('
') + if book.tags: + parts.append('
%s
\u00a0
'%', '.join(book.tags)) + if book.comments: + parts.append(comments_to_html(book.comments)) + + self.show_details_signal.emit(''.join(parts)) + + def select_index(self, index): + if not index.isValid(): + index = self.model().index(0, 0) + book = self.model().data(index, Qt.UserRole) + self.book_selected.emit(book) + + def get_result(self): + self.select_index(self.currentIndex()) + +# }}} 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) @@ -109,7 +236,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) @@ -122,17 +249,42 @@ 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: import traceback self.error = traceback.format_exc() +# }}} -class IdentifyWidget(QWidget): +class IdentifyWidget(QWidget): # {{{ + + rejected = pyqtSignal() + results_found = pyqtSignal() + book_selected = pyqtSignal(object) def __init__(self, log, parent=None): QWidget.__init__(self, parent) @@ -150,11 +302,15 @@ class IdentifyWidget(QWidget): l.addWidget(self.top, 0, 0) self.results_view = ResultsView(self) + self.results_view.book_selected.connect(self.book_selected.emit) + self.get_result = self.results_view.get_result l.addWidget(self.results_view, 1, 0) self.comments_view = Comments(self) l.addWidget(self.comments_view, 1, 1) + self.results_view.show_details_signal.connect(self.comments_view.show_data) + self.query = QLabel('download starting...') f = self.query.font() f.setPointSize(f.pointSize()-2) @@ -197,7 +353,50 @@ 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) + 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.worker.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 + + 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)) + + self.results_found.emit() + + + def cancel(self): + self.abort.set() +# }}} class FullFetch(QDialog): # {{{ @@ -213,16 +412,44 @@ class FullFetch(QDialog): # {{{ self.setLayout(l) l.addWidget(self.stack) - self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) + self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) l.addWidget(self.bb) self.bb.rejected.connect(self.reject) + self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole) + self.next_button.setDefault(True) + self.next_button.setEnabled(False) + self.next_button.clicked.connect(self.next_clicked) + self.ok_button = self.bb.button(self.bb.Ok) + self.ok_button.setVisible(False) + self.ok_button.clicked.connect(self.ok_clicked) self.identify_widget = IdentifyWidget(log, self) + self.identify_widget.rejected.connect(self.reject) + self.identify_widget.results_found.connect(self.identify_results_found) + self.identify_widget.book_selected.connect(self.book_selected) self.stack.addWidget(self.identify_widget) self.resize(850, 500) + def book_selected(self, book): + print (book) + self.next_button.setVisible(False) + self.ok_button.setVisible(True) + def accept(self): - # Prevent pressing Enter from closing the dialog + # Prevent the usual dialog accept mechanisms from working + pass + + def reject(self): + self.identify_widget.cancel() + return QDialog.reject(self) + + def identify_results_found(self): + self.next_button.setEnabled(True) + + def next_clicked(self, *args): + self.identify_widget.get_result() + + def ok_clicked(self, *args): pass def start(self, title=None, authors=None, identifiers={}): 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'), 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()