Updated icon for ODT

This commit is contained in:
Matt Perry 2011-08-06 22:47:26 -07:00
commit c7fb6159f3
30 changed files with 1005 additions and 568 deletions

View File

@ -1,3 +1,157 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
economist.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
from collections import OrderedDict
import time, re
class Economist(BasicNewsRecipe):
title = 'The Economist'
language = 'en'
__author__ = "Kovid Goyal"
INDEX = 'http://www.economist.com/printedition'
description = ('Global news and current affairs from a European'
' perspective. Best downloaded on Friday mornings (GMT)')
extra_css = '.headline {font-size: x-large;} \n h2 { font-size: small; } \n h1 { font-size: medium; }'
oldest_article = 7.0
cover_url = 'http://media.economist.com/sites/default/files/imagecache/print-cover-thumbnail/print-covers/currentcoverus_large.jpg'
#cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
remove_tags = [
dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
dict(attrs={'class':['dblClkTrk', 'ec-article-info',
'share_inline_header', 'related-items']}),
{'class': lambda x: x and 'share-links-header' in x},
]
keep_only_tags = [dict(id='ec-article-body')]
needs_subscription = False
no_stylesheets = True
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
lambda x:'</html>')]
'''
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br.open('http://www.economist.com')
req = mechanize.Request(
'http://www.economist.com/members/members.cfm?act=exec_login',
headers = {
'Referer':'http://www.economist.com/',
},
data=urllib.urlencode({
'logging_in' : 'Y',
'returnURL' : '/',
'email_address': self.username,
'fakepword' : 'Password',
'pword' : self.password,
'x' : '0',
'y' : '0',
}))
br.open(req).read()
return br
'''
def parse_index(self):
try:
return self.economist_parse_index()
except:
raise
self.log.warn(
'Initial attempt to parse index failed, retrying in 30 seconds')
time.sleep(30)
return self.economist_parse_index()
def economist_parse_index(self):
soup = self.index_to_soup(self.INDEX)
div = soup.find('div', attrs={'class':'issue-image'})
if div is not None:
img = div.find('img', src=True)
if img is not None:
self.cover_url = img['src']
feeds = OrderedDict()
for section in soup.findAll(attrs={'class':lambda x: x and 'section' in
x}):
h4 = section.find('h4')
if h4 is None:
continue
section_title = self.tag_to_string(h4).strip()
if not section_title:
continue
self.log('Found section: %s'%section_title)
articles = []
for h5 in section.findAll('h5'):
article_title = self.tag_to_string(h5).strip()
if not article_title:
continue
data = h5.findNextSibling(attrs={'class':'article'})
if data is None: continue
a = data.find('a', href=True)
if a is None: continue
url = a['href']
if url.startswith('/'): url = 'http://www.economist.com'+url
url += '/print'
article_title += ': %s'%self.tag_to_string(a).strip()
articles.append({'title':article_title, 'url':url,
'description':'', 'date':''})
if not articles:
# We have last or first section
for art in section.findAll(attrs={'class':'article'}):
a = art.find('a', href=True)
if a is not None:
url = a['href']
if url.startswith('/'): url = 'http://www.economist.com'+url
url += '/print'
title = self.tag_to_string(a)
if title:
articles.append({'title':title, 'url':url,
'description':'', 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
ans = [(key, val) for key, val in feeds.iteritems()]
if not ans:
raise Exception('Could not find any articles, either the '
'economist.com server is having trouble and you should '
'try later or the website format has changed and the '
'recipe needs to be updated.')
return ans
def eco_find_image_tables(self, soup):
for x in soup.findAll('table', align=['right', 'center']):
if len(x.findAll('font')) in (1,2) and len(x.findAll('img')) == 1:
yield x
def postprocess_html(self, soup, first):
body = soup.find('body')
for name, val in body.attrs:
del body[name]
for table in list(self.eco_find_image_tables(soup)):
caption = table.find('font')
img = table.find('img')
div = Tag(soup, 'div')
div['style'] = 'text-align:left;font-size:70%'
ns = NavigableString(self.tag_to_string(caption))
div.insert(0, ns)
div.insert(1, Tag(soup, 'br'))
del img['width']
del img['height']
img.extract()
div.insert(2, img)
table.replaceWith(div)
return soup
'''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.utils.threadpool import ThreadPool, makeRequests from calibre.utils.threadpool import ThreadPool, makeRequests
from calibre.ebooks.BeautifulSoup import Tag, NavigableString from calibre.ebooks.BeautifulSoup import Tag, NavigableString
@ -145,3 +299,5 @@ class Economist(BasicNewsRecipe):
div.insert(2, img) div.insert(2, img)
table.replaceWith(div) table.replaceWith(div)
return soup return soup
'''

View File

@ -38,6 +38,7 @@ class WallStreetJournal(BasicNewsRecipe):
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]), dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]),
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]}, {'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
dict(rel='shortcut icon'), dict(rel='shortcut icon'),
{'class':lambda x: x and 'sTools' in x},
] ]
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},] remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]

View File

@ -40,6 +40,7 @@ class WallStreetJournal(BasicNewsRecipe):
dict(name='div', attrs={'data-flash-settings':True}), dict(name='div', attrs={'data-flash-settings':True}),
{'class':['insetContent embedType-interactive insetCol3wide','insetCol6wide','insettipUnit']}, {'class':['insetContent embedType-interactive insetCol3wide','insetCol6wide','insettipUnit']},
dict(rel='shortcut icon'), dict(rel='shortcut icon'),
{'class':lambda x: x and 'sTools' in x},
] ]
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},] remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -1710,7 +1710,6 @@ class MobiWriter(object):
''' '''
from calibre.ebooks.oeb.base import TOC from calibre.ebooks.oeb.base import TOC
items = list(self._oeb.toc.iterdescendants()) items = list(self._oeb.toc.iterdescendants())
items = [i for i in items if i.depth == 1]
offsets = {i:self._id_offsets.get(i.href, -1) for i in items if i.href} offsets = {i:self._id_offsets.get(i.href, -1) for i in items if i.href}
items = [i for i in items if offsets[i] > -1] items = [i for i in items if offsets[i] > -1]
items.sort(key=lambda i:offsets[i]) items.sort(key=lambda i:offsets[i])

View File

@ -106,6 +106,10 @@ class InterfaceAction(QObject):
self.gui.addAction(self.menuless_qaction) self.gui.addAction(self.menuless_qaction)
self.genesis() self.genesis()
@property
def unique_name(self):
return u'%s(%s)'%(self.__class__.__name__, self.name)
def create_action(self, spec=None, attr='qaction'): def create_action(self, spec=None, attr='qaction'):
if spec is None: if spec is None:
spec = self.action_spec spec = self.action_spec
@ -125,13 +129,20 @@ class InterfaceAction(QObject):
a.setToolTip(text) a.setToolTip(text)
a.setStatusTip(text) a.setStatusTip(text)
a.setWhatsThis(text) a.setWhatsThis(text)
if shortcut: keys = ()
a = ma if attr == 'qaction' else action shortcut_action = action
if isinstance(shortcut, list): desc = tooltip if tooltip else None
a.setShortcuts(shortcut) if attr == 'qaction':
else: shortcut_action = ma
a.setShortcut(shortcut) if shortcut is not None:
setattr(self, attr, action) keys = ((shortcut,) if isinstance(shortcut, basestring) else
tuple(shortcut))
self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr,
unicode(shortcut_action.text()), default_keys=keys,
action=shortcut_action, description=desc)
if attr is not None:
setattr(self, attr, action)
if attr == 'qaction' and self.action_add_menu: if attr == 'qaction' and self.action_add_menu:
menu = QMenu() menu = QMenu()
action.setMenu(menu) action.setMenu(menu)
@ -139,6 +150,30 @@ class InterfaceAction(QObject):
menu.addAction(self.menuless_qaction) menu.addAction(self.menuless_qaction)
return action return action
def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None,
description=None, triggered=None):
ac = menu.addAction(text)
if icon is not None:
if not isinstance(icon, QIcon):
icon = QIcon(I(icon))
ac.setIcon(icon)
keys = ()
if shortcut is not None and shortcut is not False:
keys = ((shortcut,) if isinstance(shortcut, basestring) else
tuple(shortcut))
unique_name = '%s : menu action : %s'%(self.unique_name, unique_name)
if description is not None:
ac.setToolTip(description)
ac.setStatusTip(description)
ac.setWhatsThis(description)
if shortcut is not False:
self.gui.keyboard.register_shortcut(unique_name,
unicode(text), default_keys=keys,
action=ac, description=description)
if triggered is not None:
ac.triggered.connect(triggered)
return ac
def load_resources(self, names): def load_resources(self, names):
''' '''
If this plugin comes in a ZIP file (user added plugin), this method If this plugin comes in a ZIP file (user added plugin), this method

View File

@ -54,20 +54,22 @@ class AddAction(InterfaceAction):
def genesis(self): def genesis(self):
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book) self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
self.add_menu = self.qaction.menu() self.add_menu = self.qaction.menu()
self.add_menu.addAction(_('Add books from directories, including ' ma = partial(self.create_menu_action, self.add_menu)
ma('recursive-single', _('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook ' 'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'), 'file is the same book in a different format)')).triggered.connect(
self.add_recursive_single) self.add_recursive_single)
self.add_menu.addAction(_('Add books from directories, including ' ma('recursive-multiple', _('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every ' 'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'), self.add_recursive_multiple) 'ebook file is a different book)')).triggered.connect(
self.add_recursive_multiple)
self.add_menu.addSeparator() self.add_menu.addSeparator()
self.add_menu.addAction(_('Add Empty book. (Book entry with no ' ma('add-empty', _('Add Empty book. (Book entry with no formats)'),
'formats)'), self.add_empty, _('Shift+Ctrl+E')) shortcut=_('Shift+Ctrl+E')).triggered.connect(self.add_empty)
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn) ma('add-isbn', _('Add from ISBN')).triggered.connect(self.add_from_isbn)
self.add_menu.addSeparator() self.add_menu.addSeparator()
self.add_menu.addAction(_('Add files to selected book records'), ma('add-formats', _('Add files to selected book records'),
self.add_formats, _('Shift+A')) triggered=self.add_formats, shortcut=_('Shift+A'))
self.qaction.triggered.connect(self.add_books) self.qaction.triggered.connect(self.add_books)
@ -82,7 +84,8 @@ class AddAction(InterfaceAction):
view = self.gui.library_view view = self.gui.library_view
rows = view.selectionModel().selectedRows() rows = view.selectionModel().selectedRows()
if not rows: if not rows:
return return error_dialog(self.gui, _('No books selected'),
_('Cannot add files as no books are selected'), show=True)
ids = [view.model().id(r) for r in rows] ids = [view.model().id(r) for r in rows]
if len(ids) > 1 and not question_dialog(self.gui, if len(ids) > 1 and not question_dialog(self.gui,

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os import os
from functools import partial from functools import partial
from PyQt4.Qt import QModelIndex, QIcon from PyQt4.Qt import QModelIndex
from calibre.gui2 import error_dialog, Dispatcher from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook
@ -25,17 +25,19 @@ class ConvertAction(InterfaceAction):
action_add_menu = True action_add_menu = True
def genesis(self): def genesis(self):
cm = self.qaction.menu() m = self.convert_menu = self.qaction.menu()
cm.addAction(self.qaction.icon(), _('Convert individually'), partial(self.convert_ebook, cm = partial(self.create_menu_action, self.convert_menu)
cm('convert-individual', _('Convert individually'),
icon=self.qaction.icon(), triggered=partial(self.convert_ebook,
False, bulk=False)) False, bulk=False))
cm.addAction(_('Bulk convert'), cm('convert-bulk', _('Bulk convert'),
partial(self.convert_ebook, False, bulk=True)) triggered=partial(self.convert_ebook, False, bulk=True))
cm.addSeparator() m.addSeparator()
ac = cm.addAction(QIcon(I('catalog.png')), cm('create-catalog',
_('Create a catalog of the books in your calibre library')) _('Create a catalog of the books in your calibre library'),
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog) icon='catalog.png', shortcut=False,
triggered=self.gui.iactions['Generate Catalog'].generate_catalog)
self.qaction.triggered.connect(self.convert_ebook) self.qaction.triggered.connect(self.convert_ebook)
self.convert_menu = cm
self.conversion_jobs = {} self.conversion_jobs = {}
def location_selected(self, loc): def location_selected(self, loc):

View File

@ -90,21 +90,23 @@ class DeleteAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.delete_books) self.qaction.triggered.connect(self.delete_books)
self.delete_menu = self.qaction.menu() self.delete_menu = self.qaction.menu()
self.delete_menu.addAction( m = partial(self.create_menu_action, self.delete_menu)
m('delete-specific',
_('Remove files of a specific format from selected books..'), _('Remove files of a specific format from selected books..'),
self.delete_selected_formats) triggered=self.delete_selected_formats)
self.delete_menu.addAction( m('delete-except',
_('Remove all formats from selected books, except...'), _('Remove all formats from selected books, except...'),
self.delete_all_but_selected_formats) triggered=self.delete_all_but_selected_formats)
self.delete_menu.addAction( m('delete-all',
_('Remove all formats from selected books'), _('Remove all formats from selected books'),
self.delete_all_formats) triggered=self.delete_all_formats)
self.delete_menu.addAction( m('delete-covers',
_('Remove covers from selected books'), self.delete_covers) _('Remove covers from selected books'),
triggered=self.delete_covers)
self.delete_menu.addSeparator() self.delete_menu.addSeparator()
self.delete_menu.addAction( m('delete-matching',
_('Remove matching books from device'), _('Remove matching books from device'),
self.remove_matching_books_from_device) triggered=self.remove_matching_books_from_device)
self.qaction.setMenu(self.delete_menu) self.qaction.setMenu(self.delete_menu)
self.delete_memory = {} self.delete_memory = {}

View File

@ -60,6 +60,15 @@ class ShareConnMenu(QMenu): # {{{
self.email_actions = [] self.email_actions = []
if hasattr(parent, 'keyboard'):
r = parent.keyboard.register_shortcut
prefix = 'Share/Connect Menu '
for attr in ('folder', 'bambook', 'itunes'):
if not (iswindows or isosx) and attr == 'itunes':
continue
ac = getattr(self, 'connect_to_%s_action'%attr)
r(prefix + attr, unicode(ac.text()), action=ac)
def server_state_changed(self, running): def server_state_changed(self, running):
text = _('Start Content Server') text = _('Start Content Server')
if running: if running:

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os import os
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer from PyQt4.Qt import QMenu, QModelIndex, QTimer
from calibre.gui2 import error_dialog, Dispatcher, question_dialog from calibre.gui2 import error_dialog, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
@ -27,37 +27,38 @@ class EditMetadataAction(InterfaceAction):
action_add_menu = True action_add_menu = True
def genesis(self): def genesis(self):
self.create_action(spec=(_('Merge book records'), 'merge_books.png',
None, _('M')), attr='action_merge')
md = self.qaction.menu() md = self.qaction.menu()
md.addAction(self.qaction.icon(), _('Edit metadata individually'), cm = partial(self.create_menu_action, md)
partial(self.edit_metadata, False, bulk=False)) cm('individual', _('Edit metadata individually'), icon=self.qaction.icon(),
triggered=partial(self.edit_metadata, False, bulk=False))
md.addSeparator() md.addSeparator()
md.addAction(_('Edit metadata in bulk'), cm('bulk', _('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True)) triggered=partial(self.edit_metadata, False, bulk=True))
md.addSeparator() md.addSeparator()
md.addAction(_('Download metadata and covers'), self.download_metadata, cm('download', _('Download metadata and covers'),
Qt.ControlModifier+Qt.Key_D) triggered=partial(self.download_metadata, ids=None),
shortcut='Ctrl+D')
self.metadata_menu = md self.metadata_menu = md
mb = QMenu() mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'), cm2 = partial(self.create_menu_action, mb)
self.merge_books) cm2('merge delete', _('Merge into first selected book - delete others'),
triggered=self.merge_books)
mb.addSeparator() mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'), cm2('merge keep', _('Merge into first selected book - keep others'),
partial(self.merge_books, safe_merge=True), triggered=partial(self.merge_books, safe_merge=True),
Qt.AltModifier+Qt.Key_M) shortcut='Alt+M')
mb.addSeparator() mb.addSeparator()
mb.addAction(_('Merge only formats into first selected book - delete others'), cm2('merge formats', _('Merge only formats into first selected book - delete others'),
partial(self.merge_books, merge_only_formats=True), triggered=partial(self.merge_books, merge_only_formats=True),
Qt.AltModifier+Qt.ShiftModifier+Qt.Key_M) shortcut='Alt+Shift+M')
self.merge_menu = mb self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator() md.addSeparator()
md.addAction(self.action_merge) self.action_merge = cm('merge', _('Merge book records'), icon='merge_books.png',
shortcut=_('M'), triggered=self.merge_books)
self.action_merge.setMenu(mb)
self.qaction.triggered.connect(self.edit_metadata) self.qaction.triggered.connect(self.edit_metadata)
self.action_merge.triggered.connect(self.merge_books)
def location_selected(self, loc): def location_selected(self, loc):
enabled = loc == 'library' enabled = loc == 'library'
@ -417,7 +418,7 @@ class EditMetadataAction(InterfaceAction):
db.set_custom(dest_id, dest_value, num=colnum) db.set_custom(dest_id, dest_value, num=colnum)
if db.field_metadata[key]['datatype'] in \ if db.field_metadata[key]['datatype'] in \
('bool', 'int', 'float', 'rating', 'datetime') \ ('bool', 'int', 'float', 'rating', 'datetime') \
and not dest_value: and dest_value is None:
db.set_custom(dest_id, src_value, num=colnum) db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'series' \ if db.field_metadata[key]['datatype'] == 'series' \
and not dest_value: and not dest_value:

View File

@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from functools import partial
from PyQt4.Qt import QIcon, Qt from PyQt4.Qt import QIcon, Qt
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
@ -21,18 +23,17 @@ class PreferencesAction(InterfaceAction):
def genesis(self): def genesis(self):
pm = self.qaction.menu() pm = self.qaction.menu()
cm = partial(self.create_menu_action, pm)
if isosx: if isosx:
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config) pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'), cm('welcome wizard', _('Run welcome wizard'),
self.gui.run_wizard) icon='wizard.png', triggered=self.gui.run_wizard)
pm.addAction(QIcon(I('plugins/plugin_updater.png')), cm('plugin updater', _('Get plugins to enhance calibre'),
_('Get plugins to enhance calibre'), self.get_plugins) icon='plugins/plugin_updater.png', triggered=self.get_plugins)
if not DEBUG: if not DEBUG:
pm.addSeparator() pm.addSeparator()
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'), cm('restart', _('Restart in debug mode'), icon='debug.png',
self.debug_restart) triggered=self.debug_restart, shortcut='Ctrl+Shift+R')
ac.setShortcut('Ctrl+Shift+R')
self.gui.addAction(ac)
self.preferences_menu = pm self.preferences_menu = pm
for x in (self.gui.preferences_action, self.qaction): for x in (self.gui.preferences_action, self.qaction):

View File

@ -44,15 +44,16 @@ class SaveToDiskAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.save_to_disk) self.qaction.triggered.connect(self.save_to_disk)
self.save_menu = self.qaction.menu() self.save_menu = self.qaction.menu()
self.save_menu.addAction(_('Save to disk in a single directory'), cm = partial(self.create_menu_action, self.save_menu)
partial(self.save_to_single_dir, False)) cm('single dir', _('Save to disk in a single directory'),
self.save_menu.addAction(_('Save only %s format to disk')% triggered=partial(self.save_to_single_dir, False))
cm('single format', _('Save only %s format to disk')%
prefs['output_format'].upper(), prefs['output_format'].upper(),
partial(self.save_single_format_to_disk, False)) triggered=partial(self.save_single_format_to_disk, False))
self.save_menu.addAction( cm('single dir and format',
_('Save only %s format to disk in a single directory')% _('Save only %s format to disk in a single directory')%
prefs['output_format'].upper(), prefs['output_format'].upper(),
partial(self.save_single_fmt_to_single_dir, False)) triggered=partial(self.save_single_fmt_to_single_dir, False))
self.save_sub_menu = SaveMenu(self.gui) self.save_sub_menu = SaveMenu(self.gui)
self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu) self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu)
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk) self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
@ -114,10 +115,7 @@ class SaveToDiskAction(InterfaceAction):
opts.save_cover = False opts.save_cover = False
opts.write_opf = False opts.write_opf = False
opts.template = opts.send_template opts.template = opts.send_template
if single_dir: opts.single_dir = single_dir
opts.template = opts.template.split('/')[-1].strip()
if not opts.template:
opts.template = '{title} - {authors}'
self._saver = Saver(self.gui, self.gui.library_view.model().db, self._saver = Saver(self.gui, self.gui.library_view.model().db,
Dispatcher(self._books_saved), rows, path, opts, Dispatcher(self._books_saved), rows, path, opts,
spare_server=self.gui.spare_server) spare_server=self.gui.spare_server)

View File

@ -24,16 +24,21 @@ class StoreAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.do_search) self.qaction.triggered.connect(self.do_search)
self.store_menu = self.qaction.menu() self.store_menu = self.qaction.menu()
self.load_menu() cm = partial(self.create_menu_action, self.store_menu)
for x, t in [('author', _('author')), ('title', _('title')),
def load_menu(self): ('book', _('book'))]:
self.store_menu.clear() func = getattr(self, 'search_%s'%('author_title' if x == 'book'
self.store_menu.addAction(self.menuless_qaction) else x))
self.store_menu.addAction(_('Search for this author'), self.search_author) ac = cm(x, _('Search for this %s'%t), triggered=func)
self.store_menu.addAction(_('Search for this title'), self.search_title) setattr(self, 'action_search_by_'+x, ac)
self.store_menu.addAction(_('Search for this book'), self.search_author_title)
self.store_menu.addSeparator() self.store_menu.addSeparator()
self.store_list_menu = self.store_menu.addMenu(_('Stores')) self.store_list_menu = self.store_menu.addMenu(_('Stores'))
self.load_menu()
self.store_menu.addSeparator()
cm('choose stores', _('Choose stores'), triggered=self.choose)
def load_menu(self):
self.store_list_menu.clear()
icon = QIcon() icon = QIcon()
icon.addFile(I('donate.png'), QSize(16, 16)) icon.addFile(I('donate.png'), QSize(16, 16))
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()): for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
@ -41,8 +46,6 @@ class StoreAction(InterfaceAction):
self.store_list_menu.addAction(icon, n, partial(self.open_store, p)) self.store_list_menu.addAction(icon, n, partial(self.open_store, p))
else: else:
self.store_list_menu.addAction(n, partial(self.open_store, p)) self.store_list_menu.addAction(n, partial(self.open_store, p))
self.store_menu.addSeparator()
self.store_menu.addAction(_('Choose stores'), self.choose)
def do_search(self): def do_search(self):
return self.search() return self.search()

View File

@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, time import os, time
from functools import partial
from PyQt4.Qt import Qt, QAction, pyqtSignal from PyQt4.Qt import Qt, QAction, pyqtSignal
@ -43,36 +44,33 @@ class ViewAction(InterfaceAction):
self.qaction.triggered.connect(self.view_book) self.qaction.triggered.connect(self.view_book)
self.view_action = self.menuless_qaction self.view_action = self.menuless_qaction
self.view_menu = self.qaction.menu() self.view_menu = self.qaction.menu()
ac = self.view_specific_action = QAction(_('View specific format'), cm = partial(self.create_menu_action, self.view_menu)
self.gui) self.view_specific_action = cm('specific', _('View specific format'),
ac.setShortcut(Qt.AltModifier+Qt.Key_V) shortcut='Alt+V', triggered=self.view_specific_format)
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection) self.action_pick_random = cm('pick random', _('Read a random book'),
ac = self.create_action(spec=(_('Read a random book'), 'random.png', icon='random.png', triggered=self.view_random)
None, None), attr='action_pick_random') self.clear_sep1 = self.view_menu.addSeparator()
ac.triggered.connect(self.view_random) self.clear_sep2 = self.view_menu.addSeparator()
ac = self.clear_history_action = QAction( self.clear_history_action = cm('clear history',
_('Clear recently viewed list'), self.gui) _('Clear recently viewed list'), triggered=self.clear_history)
ac.triggered.connect(self.clear_history) self.history_actions = [self.clear_sep1]
def initialization_complete(self): def initialization_complete(self):
self.build_menus(self.gui.current_db) self.build_menus(self.gui.current_db)
def build_menus(self, db): def build_menus(self, db):
self.view_menu.clear() for ac in self.history_actions:
self.view_menu.addAction(self.view_action) self.view_menu.removeAction(ac)
self.view_menu.addAction(self.view_specific_action)
self.view_menu.addSeparator()
self.view_menu.addAction(self.action_pick_random)
self.history_actions = [] self.history_actions = []
history = db.prefs.get('gui_view_history', []) history = db.prefs.get('gui_view_history', [])
if history: if history:
self.view_menu.addSeparator() self.view_menu.insertAction(self.clear_sep2, self.clear_sep1)
self.history_actions.append(self.clear_sep1)
for id_, title in history: for id_, title in history:
ac = HistoryAction(id_, title, self.view_menu) ac = HistoryAction(id_, title, self.view_menu)
self.view_menu.addAction(ac) self.view_menu.insertAction(self.clear_sep2, ac)
ac.view_historical.connect(self.view_historical) ac.view_historical.connect(self.view_historical)
self.view_menu.addSeparator() self.history_actions.append(ac)
self.view_menu.addAction(self.clear_history_action)
def clear_history(self): def clear_history(self):
db = self.gui.current_db db = self.gui.current_db

View File

@ -66,8 +66,8 @@ class EbookDownload(object):
raise Exception(_('Not a support ebook format.')) raise Exception(_('Not a support ebook format.'))
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
with open(filename) as f: with open(filename, 'rb') as f:
mi = get_metadata(f, ext) mi = get_metadata(f, ext, force_read_metadata=True)
mi.tags.extend(tags) mi.tags.extend(tags)
id = gui.library_view.model().db.create_book_entry(mi) id = gui.library_view.model().db.create_book_entry(mi)

View File

@ -218,7 +218,7 @@ class LayoutMixin(object): # {{{
self.bd_splitter = Splitter('book_details_splitter', self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.png'), _('Book Details'), I('book.png'),
orientation=Qt.Vertical, parent=self, side_index=1, orientation=Qt.Vertical, parent=self, side_index=1,
shortcut=_('Alt+D')) shortcut=_('Shift+Alt+D'))
self.bd_splitter.addWidget(self.stack) self.bd_splitter.addWidget(self.stack)
self.bd_splitter.addWidget(self.book_details) self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False) self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from collections import OrderedDict
from PyQt4.Qt import (QObject, QKeySequence)
from calibre.utils.config import JSONConfig
from calibre.constants import DEBUG
from calibre import prints
class NameConflict(ValueError):
pass
class Manager(QObject):
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.config = JSONConfig('shortcuts/main')
self.custom_keys_map = {}
self.shortcuts = OrderedDict()
self.keys_map = {}
for unique_name, keys in self.config.get(
'map', {}).iteritems():
self.custom_keys_map[unique_name] = tuple(keys)
def register_shortcut(self, unique_name, name, default_keys=(),
description=None, action=None):
if unique_name in self.shortcuts:
name = self.shortcuts[unique_name]['name']
raise NameConflict('Shortcut for %r already registered by %s'%(
unique_name, name))
shortcut = {'name':name, 'desc':description, 'action': action,
'default_keys':tuple(default_keys)}
self.shortcuts[unique_name] = shortcut
def finalize(self):
seen = {}
for unique_name, shortcut in self.shortcuts.iteritems():
custom_keys = self.custom_keys_map.get(unique_name, None)
if custom_keys is None:
candidates = shortcut['default_keys']
else:
candidates = custom_keys
keys = []
for x in candidates:
ks = QKeySequence(x, QKeySequence.PortableText)
x = unicode(ks.toString(QKeySequence.PortableText))
if x in seen:
if DEBUG:
prints('Key %r for shortcut %s is already used by'
' %s, ignoring'%(x, shortcut['name'], seen[x]['name']))
continue
seen[x] = shortcut
keys.append(ks)
keys = tuple(keys)
#print (111111, unique_name, candidates, keys)
self.keys_map[unique_name] = keys
ac = shortcut['action']
if ac is not None:
ac.setShortcuts(list(keys))

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import (QIcon, Qt, QWidget, QSize, from PyQt4.Qt import (QIcon, Qt, QWidget, QSize,
pyqtSignal, QToolButton, QMenu, pyqtSignal, QToolButton, QMenu, QAction,
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup) QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
@ -178,7 +178,12 @@ class SearchBar(QWidget): # {{{
x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
parent.advanced_search_button = x = QToolButton(self) parent.advanced_search_button = x = QToolButton(self)
parent.advanced_search_button.setShortcut(_("Shift+Ctrl+F")) parent.advanced_search_toggle_action = ac = QAction(parent)
parent.addAction(ac)
parent.keyboard.register_shortcut('advanced search toggle',
_('Advanced search'), default_keys=(_("Shift+Ctrl+F"),),
action=ac)
ac.triggered.connect(x.click)
x.setIcon(QIcon(I('search.png'))) x.setIcon(QIcon(I('search.png')))
l.addWidget(x) l.addWidget(x)
x.setToolTip(_("Advanced search")) x.setToolTip(_("Advanced search"))

View File

@ -310,6 +310,12 @@ class CcNumberDelegate(QStyledItemDelegate): # {{{
editor.setDecimals(2) editor.setDecimals(2)
return editor return editor
def setModelData(self, editor, model, index):
val = editor.value()
if val == editor.minimum():
val = None
model.setData(index, QVariant(val), Qt.EditRole)
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
m = index.model() m = index.model()
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']] val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]

View File

@ -376,9 +376,12 @@ class SearchBoxMixin(object): # {{{
self.search.clear() self.search.clear()
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
self.action_focus_search = QAction(self) self.action_focus_search = QAction(self)
shortcuts = QKeySequence.keyBindings(QKeySequence.Find) shortcuts = list(
shortcuts = list(shortcuts) + [QKeySequence('/'), QKeySequence('Alt+S')] map(lambda x:unicode(x.toString()),
self.action_focus_search.setShortcuts(shortcuts) QKeySequence.keyBindings(QKeySequence.Find)))
shortcuts += ['/', 'Alt+S']
self.keyboard.register_shortcut('start search', _('Start search'),
default_keys=shortcuts, action=self.action_focus_search)
self.action_focus_search.triggered.connect(self.focus_search_box) self.action_focus_search.triggered.connect(self.focus_search_box)
self.addAction(self.action_focus_search) self.addAction(self.action_focus_search)
self.search.setStatusTip(re.sub(r'<\w+>', ' ', self.search.setStatusTip(re.sub(r'<\w+>', ' ',

View File

@ -16,7 +16,7 @@ from collections import OrderedDict
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction, from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
QMenu, QIcon, pyqtSignal, QUrl, QMenu, QIcon, pyqtSignal, QUrl,
QDialog, QSystemTrayIcon, QApplication, QKeySequence) QDialog, QSystemTrayIcon, QApplication)
from calibre import prints from calibre import prints
from calibre.constants import __appname__, isosx from calibre.constants import __appname__, isosx
@ -40,6 +40,7 @@ from calibre.gui2.init import LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_browser.ui import TagBrowserMixin from calibre.gui2.tag_browser.ui import TagBrowserMixin
from calibre.gui2.keyboard import Manager
class Listener(Thread): # {{{ class Listener(Thread): # {{{
@ -104,6 +105,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def __init__(self, opts, parent=None, gui_debug=None): def __init__(self, opts, parent=None, gui_debug=None):
global _gui global _gui
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
self.keyboard = Manager(self)
_gui = self _gui = self
self.opts = opts self.opts = opts
self.device_connected = None self.device_connected = None
@ -238,7 +240,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.eject_action.setEnabled(False) self.eject_action.setEnabled(False)
self.addAction(self.quit_action) self.addAction(self.quit_action)
self.system_tray_menu.addAction(self.quit_action) self.system_tray_menu.addAction(self.quit_action)
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q)) self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
default_keys=('Ctrl+Q',), action=self.quit_action)
self.system_tray_icon.setContextMenu(self.system_tray_menu) self.system_tray_icon.setContextMenu(self.system_tray_menu)
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit) self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate) self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
@ -249,7 +252,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.esc_action = QAction(self) self.esc_action = QAction(self)
self.addAction(self.esc_action) self.addAction(self.esc_action)
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape)) self.keyboard.register_shortcut('clear current search',
_('Clear the current search'), default_keys=('Esc',),
action=self.esc_action)
self.esc_action.triggered.connect(self.esc) self.esc_action.triggered.connect(self.esc)
####################### Start spare job server ######################## ####################### Start spare job server ########################
@ -340,6 +345,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
raise raise
self.device_manager.set_current_library_uuid(db.library_id) self.device_manager.set_current_library_uuid(db.library_id)
self.keyboard.finalize()
# Collect cycles now # Collect cycles now
gc.collect() gc.collect()

View File

@ -1063,9 +1063,16 @@ class Splitter(QSplitter):
self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label, self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label,
self) self)
self.action_toggle.triggered.connect(self.toggle_triggered) self.action_toggle.triggered.connect(self.toggle_triggered)
self.action_toggle.setShortcut(shortcut)
if parent is not None: if parent is not None:
parent.addAction(self.action_toggle) parent.addAction(self.action_toggle)
if hasattr(parent, 'keyboard'):
parent.keyboard.register_shortcut('splitter %s %s'%(name,
label), unicode(self.action_toggle.text()),
default_keys=(shortcut,), action=self.action_toggle)
else:
self.action_toggle.setShortcut(shortcut)
else:
self.action_toggle.setShortcut(shortcut)
def toggle_triggered(self, *args): def toggle_triggered(self, *args):
self.toggle_side_pane() self.toggle_side_pane()

View File

@ -121,6 +121,9 @@ def config(defaults=None):
help=_('Convert paths to lowercase.')) help=_('Convert paths to lowercase.'))
x('replace_whitespace', default=False, x('replace_whitespace', default=False,
help=_('Replace whitespace with underscores.')) help=_('Replace whitespace with underscores.'))
x('single_dir', default=False,
help=_('Save into a single directory, ignoring the template'
' directory structure'))
return c return c
def preprocess_template(template): def preprocess_template(template):
@ -131,7 +134,7 @@ def preprocess_template(template):
template = template.decode(preferred_encoding, 'replace') template = template.decode(preferred_encoding, 'replace')
return template return template
class SafeFormat(TemplateFormatter): class Formatter(TemplateFormatter):
''' '''
Provides a format function that substitutes '' for any missing value Provides a format function that substitutes '' for any missing value
''' '''
@ -165,7 +168,7 @@ class SafeFormat(TemplateFormatter):
def get_components(template, mi, id, timefmt='%b %Y', length=250, def get_components(template, mi, id, timefmt='%b %Y', length=250,
sanitize_func=ascii_filename, replace_whitespace=False, sanitize_func=ascii_filename, replace_whitespace=False,
to_lowercase=False): to_lowercase=False, safe_format=True):
tsorder = tweaks['save_template_title_series_sorting'] tsorder = tweaks['save_template_title_series_sorting']
format_args = FORMAT_ARGS.copy() format_args = FORMAT_ARGS.copy()
@ -225,8 +228,11 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args[key] = unicode(format_args[key]) format_args[key] = unicode(format_args[key])
else: else:
format_args[key] = '' format_args[key] = ''
components = SafeFormat().safe_format(template, format_args, if safe_format:
components = Formatter().safe_format(template, format_args,
'G_C-EXCEPTION!', mi) 'G_C-EXCEPTION!', mi)
else:
components = Formatter().unsafe_format(template, format_args, mi)
components = [x.strip() for x in components.split('/')] components = [x.strip() for x in components.split('/')]
components = [sanitize_func(x) for x in components if x] components = [sanitize_func(x) for x in components if x]
if not components: if not components:
@ -283,10 +289,20 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
if not formats: if not formats:
return True, id_, mi.title return True, id_, mi.title
components = get_components(opts.template, mi, id_, opts.timefmt, length, try:
components = get_components(opts.template, mi, id_, opts.timefmt, length,
ascii_filename if opts.asciiize else sanitize_file_name_unicode, ascii_filename if opts.asciiize else sanitize_file_name_unicode,
to_lowercase=opts.to_lowercase, to_lowercase=opts.to_lowercase,
replace_whitespace=opts.replace_whitespace) replace_whitespace=opts.replace_whitespace, safe_format=False)
except Exception, e:
raise ValueError(_('Failed to calculate path for '
'save to disk. Template: %s\n'
'Error: %s'%(opts.template, e)))
if opts.single_dir:
components = components[-1:]
if not components:
raise ValueError(_('Template evaluation resulted in no'
' path components. Template: %s')%opts.template)
base_path = os.path.join(root, *components) base_path = os.path.join(root, *components)
base_name = os.path.basename(base_path) base_name = os.path.basename(base_path)
dirpath = os.path.dirname(base_path) dirpath = os.path.dirname(base_path)

View File

@ -547,6 +547,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
- Toggle jobs list - Toggle jobs list
* - :kbd:`Alt+Shift+B` * - :kbd:`Alt+Shift+B`
- Toggle Cover Browser - Toggle Cover Browser
* - :kbd:`Alt+Shift+B`
- Toggle Book Details panel
* - :kbd:`Alt+Shift+T` * - :kbd:`Alt+Shift+T`
- Toggle Tag Browser - Toggle Tag Browser
* - :kbd:`Alt+A` * - :kbd:`Alt+A`

View File

@ -273,7 +273,9 @@ The following functions are available in addition to those described in single-f
* ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole. * ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole.
* ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting. * ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting.
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments. * ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
* ``strcat_max(max, string1, prefix2, string2, ...)`` -- Returns a string formed by concatenating the arguments. The returned value is initialized to string1. `Prefix, string` pairs are added to the end of the value as long as the resulting string length is less than `max`. String1 is returned even if string1 is longer than max. You can pass as many `prefix, string` pairs as you wish.
* ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``strlen(a)`` -- Returns the length of the string passed as the argument.
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``. * ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
* ``today()`` -- return a date string for today. This value is designed for use in format_date or days_between, but can be manipulated like any other string. The date is in ISO format. * ``today()`` -- return a date string for today. This value is designed for use in format_date or days_between, but can be manipulated like any other string. The date is in ISO format.

File diff suppressed because it is too large Load Diff

View File

@ -310,7 +310,16 @@ class TemplateFormatter(string.Formatter):
ans = string.Formatter.vformat(self, fmt, args, kwargs) ans = string.Formatter.vformat(self, fmt, args, kwargs)
return self.compress_spaces.sub(' ', ans).strip() return self.compress_spaces.sub(' ', ans).strip()
########## a formatter guaranteed not to throw and exception ############ ########## a formatter that throws exceptions ############
def unsafe_format(self, fmt, kwargs, book):
self.kwargs = kwargs
self.book = book
self.composite_values = {}
self.locals = {}
return self.vformat(fmt, [], kwargs).strip()
########## a formatter guaranteed not to throw an exception ############
def safe_format(self, fmt, kwargs, error_value, book): def safe_format(self, fmt, kwargs, error_value, book):
self.kwargs = kwargs self.kwargs = kwargs

View File

@ -136,6 +136,19 @@ class BuiltinStrcat(BuiltinFormatterFunction):
res += args[i] res += args[i]
return res return res
class BuiltinStrlen(BuiltinFormatterFunction):
name = 'strlen'
arg_count = 1
category = 'String Manipulation'
__doc__ = doc = _('strlen(a) -- Returns the length of the string passed as '
'the argument')
def evaluate(self, formatter, kwargs, mi, locals, a):
try:
return len(a)
except:
return -1
class BuiltinAdd(BuiltinFormatterFunction): class BuiltinAdd(BuiltinFormatterFunction):
name = 'add' name = 'add'
arg_count = 2 arg_count = 2
@ -345,6 +358,40 @@ class BuiltinSwitch(BuiltinFormatterFunction):
return args[i+1] return args[i+1]
i += 2 i += 2
class BuiltinStrcatMax(BuiltinFormatterFunction):
name = 'strcat_max'
arg_count = -1
category = 'String Manipulation'
__doc__ = doc = _('strcat_max(max, string1, prefix2, string2, ...) -- '
'Returns a string formed by concatenating the arguments. The '
'returned value is initialized to string1. `Prefix, string` '
'pairs are added to the end of the value as long as the '
'resulting string length is less than `max`. String1 is returned '
'even if string1 is longer than max. You can pass as many '
'`prefix, string` pairs as you wish.')
def evaluate(self, formatter, kwargs, mi, locals, *args):
if len(args) < 2:
raise ValueError(_('strcat_max requires 2 or more arguments'))
if (len(args) % 2) != 0:
raise ValueError(_('strcat_max requires an even number of arguments'))
try:
max = int(args[0])
except:
raise ValueError(_('first argument to strcat_max must be an integer'))
i = 2
result = args[1]
try:
while i < len(args):
if (len(result) + len(args[i]) + len(args[i+1])) > max:
break
result = result + args[i] + args[i+1]
i += 2
except:
pass
return result.strip()
class BuiltinInList(BuiltinFormatterFunction): class BuiltinInList(BuiltinFormatterFunction):
name = 'in_list' name = 'in_list'
arg_count = 5 arg_count = 5
@ -432,7 +479,7 @@ class BuiltinSwapAroundComma(BuiltinFormatterFunction):
'returns val unchanged') 'returns val unchanged')
def evaluate(self, formatter, kwargs, mi, locals, val): def evaluate(self, formatter, kwargs, mi, locals, val):
return re.sub(r'^(.*?),(.*$)', r'\2 \1', val, flags=re.I) return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip()
class BuiltinIfempty(BuiltinFormatterFunction): class BuiltinIfempty(BuiltinFormatterFunction):
name = 'ifempty' name = 'ifempty'
@ -502,7 +549,7 @@ class BuiltinListitem(BuiltinFormatterFunction):
index = int(index) index = int(index)
val = val.split(sep) val = val.split(sep)
try: try:
return val[index] return val[index].strip()
except: except:
return '' return ''
@ -620,7 +667,8 @@ class BuiltinSublist(BuiltinFormatterFunction):
return '' return ''
si = int(start_index) si = int(start_index)
ei = int(end_index) ei = int(end_index)
val = val.split(sep) # allow empty list items so counts are what the user expects
val = [v.strip() for v in val.split(sep)]
try: try:
if ei == 0: if ei == 0:
return sep.join(val[si:]) return sep.join(val[si:])
@ -955,7 +1003,8 @@ _formatter_builtins = [
BuiltinLowercase(), BuiltinMultiply(), BuiltinNot(), BuiltinLowercase(), BuiltinMultiply(), BuiltinNot(),
BuiltinOndevice(), BuiltinOr(), BuiltinPrint(), BuiltinRawField(), BuiltinOndevice(), BuiltinOr(), BuiltinPrint(), BuiltinRawField(),
BuiltinRe(), BuiltinSelect(), BuiltinShorten(), BuiltinStrcat(), BuiltinRe(), BuiltinSelect(), BuiltinShorten(), BuiltinStrcat(),
BuiltinStrcmp(), BuiltinStrInList(), BuiltinSubitems(), BuiltinStrcatMax(),
BuiltinStrcmp(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),
BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundComma(), BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundComma(),
BuiltinSwitch(), BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(), BuiltinSwitch(), BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(),
BuiltinToday(), BuiltinUppercase(), BuiltinToday(), BuiltinUppercase(),