diff --git a/resources/recipes/everett_herald.recipe b/resources/recipes/everett_herald.recipe new file mode 100644 index 0000000000..3d91836b48 --- /dev/null +++ b/resources/recipes/everett_herald.recipe @@ -0,0 +1,36 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1295088390(BasicNewsRecipe): + title = u'Everett Herald' + language = 'en' + __author__ = '77ja65' + oldest_article = 4 + max_articles_per_feed = 50 + no_stylesheets = True + masthead_url = 'http://heraldnet.com/images/hnet/jQueryComponents/jQueryNavigation/heraldnet_logo.png' + extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }' + + feeds = [(u'Local News', + u'http://heraldnet.com/section/RSS02&mime=xml'), + (u'Sports', u'http://heraldnet.com/section/RSS04&mime=xml'), + (u'Entertainment', + u'http://heraldnet.com/section/RSS07&mime=xml'), + (u'Life', u'http://heraldnet.com/section/RSS03&mime=xml'), + (u'Breaking News', + u'http://heraldnet.com/section/RSS34&mime=xml'), + (u'Seahawks', u'http://heraldnet.com/section/RSS22&mime=xml'), + (u'HeraldNet', u'http://heraldnet.com/section/RSS01&mime=xml'), + (u'Inside Everett', + u'http://heraldnet.com/section/RSS26&mime=xml') + ] + + def print_version(self, url): + return url + "&template=PrinterFriendly" + + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font- + weight:bold;font-size:large;} + h2{font-family:Arial,Helvetica,sans-serif; font- + weight:normal;font-size:small;} + ''' + diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py index aabb1b8bc4..ad7f5f117d 100644 --- a/src/calibre/ebooks/conversion/utils.py +++ b/src/calibre/ebooks/conversion/utils.py @@ -137,17 +137,17 @@ class HeuristicProcessor(object): ] ITALICIZE_STYLE_PATS = [ - r'(?msu)(?<=\s)_(?P\S[^_]{0,40}?\S)?_(?=\s)', - r'(?msu)(?<=\s)/(?P\S[^/]{0,40}?\S)?/(?=\s)', - r'(?msu)(?<=\s)~~(?P\S[^~]{0,40}?\S)?~~(?=\s)', - r'(?msu)(?<=\s)\*(?P\S[^\*]{0,40}?\S)?\*(?=\s)', - r'(?msu)(?<=\s)~(?P\S[^~]{0,40}?\S)?~(?=\s)', - r'(?msu)(?<=\s)_/(?P\S[^/_]{0,40}?\S)?/_(?=\s)', - r'(?msu)(?<=\s)_\*(?P\S[^\*_]{0,40}?\S)?\*_(?=\s)', - r'(?msu)(?<=\s)\*/(?P\S[^/\*]{0,40}?\S)?/\*(?=\s)', - r'(?msu)(?<=\s)_\*/(?P\S[^\*_]{0,40}?\S)?/\*_(?=\s)', - r'(?msu)(?<=\s)/:(?P\S[^:/]{0,40}?\S)?:/(?=\s)', - r'(?msu)(?<=\s)\|:(?P\S[^:\|]{0,40}?\S)?:\|(?=\s)', + r'(?msu)(?<=\s)_(?P\S[^_]{0,40}?\S)?_(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)/(?P\S[^/]{0,40}?\S)?/(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)~~(?P\S[^~]{0,40}?\S)?~~(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)\*(?P\S[^\*]{0,40}?\S)?\*(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)~(?P\S[^~]{0,40}?\S)?~(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)_/(?P\S[^/_]{0,40}?\S)?/_(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)_\*(?P\S[^\*_]{0,40}?\S)?\*_(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)\*/(?P\S[^/\*]{0,40}?\S)?/\*(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)_\*/(?P\S[^\*_]{0,40}?\S)?/\*_(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)/:(?P\S[^:/]{0,40}?\S)?:/(?=[\s\.,\!\?])', + r'(?msu)(?<=\s)\|:(?P\S[^:\|]{0,40}?\S)?:\|(?=[\s\.,\!\?])', ] for word in ITALICIZE_WORDS: diff --git a/src/calibre/gui2/convert/regex_builder.py b/src/calibre/gui2/convert/regex_builder.py index bdcbe3356d..bdd219d733 100644 --- a/src/calibre/gui2/convert/regex_builder.py +++ b/src/calibre/gui2/convert/regex_builder.py @@ -166,6 +166,8 @@ class RegexEdit(QWidget, Ui_Edit): def builder(self): bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self) + if bld.cancelled: + return if bld.exec_() == bld.Accepted: self.edit.setText(bld.regex.text()) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 28b5e178ac..5df69442eb 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -13,7 +13,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma device_plugins from calibre.devices.interface import DevicePlugin from calibre.devices.errors import UserFeedback, OpenFeedback -from calibre.gui2.dialogs.choose_format import ChooseFormatDialog +from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog from calibre.utils.ipc.job import BaseJob from calibre.devices.scanner import DeviceScanner from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \ @@ -826,8 +826,24 @@ class DeviceMixin(object): # {{{ fmt = None if specific: - d = ChooseFormatDialog(self, _('Choose format to send to device'), - self.device_manager.device.settings().format_map) + formats = [] + aval_out_formats = available_output_formats() + format_count = {} + for row in rows: + fmts = self.library_view.model().db.formats(row.row()) + if fmts: + for f in fmts.split(','): + f = f.lower() + if format_count.has_key(f): + format_count[f] += 1 + else: + format_count[f] = 1 + for f in self.device_manager.device.settings().format_map: + if f in format_count.keys(): + formats.append((f, _('%i of %i Books' % (format_count[f], len(rows))), True if f in aval_out_formats else False)) + elif f in aval_out_formats: + formats.append((f, _('0 of %i Books' % len(rows)), True)) + d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats) if d.exec_() != QDialog.Accepted: return if d.format(): diff --git a/src/calibre/gui2/dialogs/choose_format_device.py b/src/calibre/gui2/dialogs/choose_format_device.py new file mode 100644 index 0000000000..f5e0761e4f --- /dev/null +++ b/src/calibre/gui2/dialogs/choose_format_device.py @@ -0,0 +1,53 @@ +__license__ = 'GPL v3' +__copyright__ = '2011, John Schember ' + +from PyQt4.Qt import QDialog, QTreeWidgetItem, QIcon, SIGNAL + +from calibre.gui2 import file_icon_provider +from calibre.gui2.dialogs.choose_format_device_ui import Ui_ChooseFormatDeviceDialog + +class ChooseFormatDeviceDialog(QDialog, Ui_ChooseFormatDeviceDialog): + + def __init__(self, window, msg, formats): + ''' + formats is a list of tuples: [(format, exists, convertible)]. + format: Lower case format identifier. E.G. mobi + exists: String representing the number of books that + exist in the format. + convertible: True if the format is a convertible format. + formats should be ordered in the device's preferred format ordering. + ''' + QDialog.__init__(self, window) + Ui_ChooseFormatDeviceDialog.__init__(self) + self.setupUi(self) + self.connect(self.formats, SIGNAL('activated(QModelIndex)'), + self.activated_slot) + + self.msg.setText(msg) + for i, (format, exists, convertible) in enumerate(formats): + t_item = QTreeWidgetItem() + t_item.setIcon(0, file_icon_provider().icon_from_ext(format.lower())) + t_item.setText(0, format.upper()) + t_item.setText(1, exists) + if convertible: + t_item.setIcon(2, QIcon(I('ok.png'))) + self.formats.addTopLevelItem(t_item) + if i == 0: + self.formats.setCurrentItem(t_item) + t_item.setSelected(True) + self.formats.resizeColumnToContents(2) + self.formats.resizeColumnToContents(1) + self.formats.resizeColumnToContents(0) + self.formats.header().resizeSection(0, self.formats.header().sectionSize(0) * 2) + self._format = None + + def activated_slot(self, *args): + self.accept() + + def format(self): + return self._format + + def accept(self): + self._format = unicode(self.formats.currentItem().text(0)) + return QDialog.accept(self) + diff --git a/src/calibre/gui2/dialogs/choose_format_device.ui b/src/calibre/gui2/dialogs/choose_format_device.ui new file mode 100644 index 0000000000..a2a07e414a --- /dev/null +++ b/src/calibre/gui2/dialogs/choose_format_device.ui @@ -0,0 +1,111 @@ + + + ChooseFormatDeviceDialog + + + + 0 + 0 + 507 + 377 + + + + Choose Format + + + + :/images/mimetypes/unknown.png:/images/mimetypes/unknown.png + + + + + + + + + + + + + true + + + + 64 + 64 + + + + true + + + + Format + + + + + Existing + + + AlignLeft|AlignVCenter + + + + + Convertible + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + ChooseFormatDeviceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ChooseFormatDeviceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 6e6b553dba..2c88556d7b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -6,8 +6,7 @@ __copyright__ = '2008, Kovid Goyal ' import re, os from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \ - pyqtSignal, QDialogButtonBox -from PyQt4 import QtGui + pyqtSignal, QDialogButtonBox, QDate, QLineEdit from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor @@ -302,6 +301,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.pubdate.setSpecialValueText(_('Undefined')) self.clear_pubdate_button.clicked.connect(self.clear_pubdate) self.pubdate.dateChanged.connect(self.do_apply_pubdate) + self.adddate.setDate(QDate.currentDate()) self.adddate.setMinimumDate(UNDEFINED_QDATE) self.adddate.setSpecialValueText(_('Undefined')) self.clear_adddate_button.clicked.connect(self.clear_adddate) @@ -365,16 +365,16 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): offset = 10 self.s_r_number_of_books = min(10, len(self.ids)) for i in range(1,self.s_r_number_of_books+1): - w = QtGui.QLabel(self.tabWidgetPage3) + w = QLabel(self.tabWidgetPage3) w.setText(_('Book %d:')%i) self.testgrid.addWidget(w, i+offset, 0, 1, 1) - w = QtGui.QLineEdit(self.tabWidgetPage3) + w = QLineEdit(self.tabWidgetPage3) w.setReadOnly(True) name = 'book_%d_text'%i setattr(self, name, w) self.book_1_text.setObjectName(name) self.testgrid.addWidget(w, i+offset, 1, 1, 1) - w = QtGui.QLineEdit(self.tabWidgetPage3) + w = QLineEdit(self.tabWidgetPage3) w.setReadOnly(True) name = 'book_%d_result'%i setattr(self, name, w) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 837776cb9a..d68be3b7d6 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -515,8 +515,8 @@ class TagsModel(QAbstractItemModel): # {{{ QAbstractItemModel.__init__(self, parent) # must do this here because 'QPixmap: Must construct a QApplication - # before a QPaintDevice'. The ':' in front avoids polluting either the - # user-defined categories (':' at end) or columns namespaces (no ':'). + # before a QPaintDevice'. The ':' at the end avoids polluting either of + # the other namespaces (alpha, '#', or '@') iconmap = {} for key in category_icon_map: iconmap[key] = QIcon(I(category_icon_map[key])) @@ -690,7 +690,7 @@ class TagsModel(QAbstractItemModel): # {{{ tb_cats = self.db.field_metadata for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(), key=sort_key): - cat_name = user_cat+':' # add the ':' to avoid name collision + cat_name = '@' + user_cat # add the '@' to avoid name collision tb_cats.add_user_category(label=cat_name, name=user_cat) if len(saved_searches().names()): tb_cats.add_search_category(label='search', name=_('Searches')) @@ -997,7 +997,7 @@ class TagsModel(QAbstractItemModel): # {{{ if self.hidden_categories and self.categories[i] in self.hidden_categories: continue row_index += 1 - if key.endswith(':'): + if key.startswith('@'): # User category, so skip it. The tag will be marked in its real category continue category_item = self.root_item.children[row_index] @@ -1016,7 +1016,7 @@ class TagsModel(QAbstractItemModel): # {{{ ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) return ans - def find_node(self, key, txt, start_path): + def find_item_node(self, key, txt, start_path): ''' Search for an item (a node) in the tags browser list that matches both the key (exact case-insensitive match) and txt (contains case- @@ -1070,6 +1070,22 @@ class TagsModel(QAbstractItemModel): # {{{ break return self.path_found + def find_category_node(self, key): + ''' + Search for an category node (a top-level node) in the tags browser list + that matches the key (exact case-insensitive match). Returns the path to + the node. Paths are as in find_item_node. + ''' + if not key: + return None + + for i in xrange(self.rowCount(QModelIndex())): + idx = self.index(i, 0, QModelIndex()) + ckey = idx.internalPointer().category_key + if strcmp(ckey, key) == 0: + return self.path_for_index(idx) + return None + def show_item_at_path(self, path, box=False): ''' Scroll the browser and open categories to show the item referenced by @@ -1355,15 +1371,15 @@ class TagBrowserWidget(QWidget): # {{{ self.search_button.setFocus(True) self.item_search.lineEdit().blockSignals(False) - colon = txt.find(':') 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_node(key, txt, - self.current_find_position) + self.current_find_position = \ + model.find_item_node(key, txt, self.current_find_position) if self.current_find_position: model.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 4b66b6620f..5638bad1ee 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -319,7 +319,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.field_metadata.remove_dynamic_categories() tb_cats = self.field_metadata for user_cat in sorted(self.prefs.get('user_categories', {}).keys(), key=sort_key): - cat_name = user_cat+':' # add the ':' to avoid name collision + cat_name = '@' + user_cat # add the '@' to avoid name collision tb_cats.add_user_category(label=cat_name, name=user_cat) if len(saved_searches().names()): tb_cats.add_search_category(label='search', name=_('Searches')) @@ -1243,7 +1243,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if category in icon_map: icon = icon_map[label] else: - icon = icon_map[':custom'] + icon = icon_map['custom:'] icon_map[category] = icon datatype = cat['datatype'] @@ -1339,20 +1339,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if label in taglist and name in taglist[label]: items.append(taglist[label][name]) # else: do nothing, to not include nodes w zero counts - if len(items): - cat_name = user_cat+':' # add the ':' to avoid name collision - # Not a problem if we accumulate entries in the icon map - if icon_map is not None: - icon_map[cat_name] = icon_map[':user'] - if sort == 'popularity': - categories[cat_name] = \ - sorted(items, key=lambda x: x.count, reverse=True) - elif sort == 'name': - categories[cat_name] = \ - sorted(items, key=lambda x: sort_key(x.sort)) - else: - categories[cat_name] = \ - sorted(items, key=lambda x:x.avg_rating, reverse=True) + cat_name = '@' + user_cat # add the '@' to avoid name collision + # Not a problem if we accumulate entries in the icon map + if icon_map is not None: + icon_map[cat_name] = icon_map['user:'] + if sort == 'popularity': + categories[cat_name] = \ + sorted(items, key=lambda x: x.count, reverse=True) + elif sort == 'name': + categories[cat_name] = \ + sorted(items, key=lambda x: sort_key(x.sort)) + else: + categories[cat_name] = \ + sorted(items, key=lambda x:x.avg_rating, reverse=True) #### Finally, the saved searches category #### items = [] diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 2a9b7e7003..e9402d1227 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -16,7 +16,7 @@ class TagsIcons(dict): ''' category_icons = ['authors', 'series', 'formats', 'publisher', 'rating', - 'news', 'tags', ':custom', ':user', 'search',] + 'news', 'tags', 'custom:', 'user:', 'search',] def __init__(self, icon_dict): for a in self.category_icons: if a not in icon_dict: @@ -31,8 +31,8 @@ category_icon_map = { 'rating' : 'rating.png', 'news' : 'news.png', 'tags' : 'tags.png', - ':custom' : 'column.png', - ':user' : 'drawer.png', + 'custom:' : 'column.png', + 'user:' : 'drawer.png', 'search' : 'search.png' } diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index c84800116d..3718f830f3 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -478,6 +478,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes - Focus the search bar * - :kbd:`Shift+Ctrl+F` - Open the advanced search dialog + * - :kbd:`Esc` + - Clear the current search * - :kbd:`N or F3` - Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked) * - :kbd:`Shift+N or Shift+F3`