Merge from main branch

This commit is contained in:
Tom Scholl 2011-04-08 23:00:32 +00:00
commit 2c054f7505
98 changed files with 40024 additions and 34704 deletions

View file

@ -19,6 +19,65 @@
# new recipes:
# - title:
- version: 0.7.54
date: 2011-04-08
new features:
- title: "New output format, HTMLZ which is a single HTML file with its associated images/stylesheets in a zipped up file"
description: "Useful when you want to convert your ebook into a single HTML file for easy editing. Note that this output plugin is still new and needs testing"
- title: "When dealing with ZIP/RAR archives, use the file header rather than the file extension to detrmine the file type, when possible. This fixes the common case of CBZ files being actually cbr files and vice versa"
- title: "Support for the Motorola Atrix"
- title: "Allow the icons in the toolbar to be turned off completely via Preferences->Look & Feel"
- title: "When downloading metadata use the gzip transfer encoding when possible for a speedup."
tickets: [749304]
bug fixes:
- title: "Conversion pipeline: Workaround for bug in lxml that causes a massive mem leak on windows and OS X when the input document contains non ASCII CSS selectors."
tickets: [754555]
- title: "Conversion pipeline: Handle inline <style> tags that put all the actual CSS inside an XML comment."
tickets: [750063]
- title: "The 'Choose Library' button now shows its popup menu when you already have more than one library instead of the dialog to create a new library"
tickets: [754154]
- title: "Apply all content server setting when clicking the Start Server button in Preferences->Sharing over the net"
tickets: [753122]
- title: "Fix content server breaking if its restriction is set to a saved search that was deleted"
tickets: [751950]
- title: "Fix detection of PocketBook with 2.0.6 firmware on windows"
tickets: [750336]
- title: "ODT Input: Fix handling of the <text:s> element."
tickets: [749655]
- title: "MOBI Output: Don't use self closed tags"
- title: "Fix book details popup becoming too tall if there is a lot of metadata"
- title: "Fix new PDF engine crashing on PDF files with embedded fonts with null names"
improved recipes:
- Kommersant
- Perfil
- Times of India
- IHT
- Guardian
new recipes:
- title: "Al Ahram"
authors: Hassan Williamson
- title: "F-Secure and developpez.com"
authors: louhike
- version: 0.7.53
date: 2011-04-01

62
recipes/al_ahram.recipe Normal file
View file

@ -0,0 +1,62 @@
# coding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2011, Hassan Williamson <haz at hazrpg.co.uk>'
'''
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'),
]

View file

@ -2,7 +2,7 @@
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.53'
__version__ = '0.7.54'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re, importlib

View file

@ -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

View file

@ -23,6 +23,10 @@
# Setup gprefs {{{
gprefs = JSONConfig('gui')
gprefs.defaults['action-layout-menubar'] = ()
gprefs.defaults['action-layout-menubar-device'] = ()
gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',

View file

@ -75,7 +75,7 @@ class InterfaceAction(QObject):
dont_remove_from = frozenset([])
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
'context-menu-device', 'toolbar-child'])
'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device'])
#: Type of action
#: 'current' means acts on the current view
@ -145,11 +145,10 @@ def load_resources(self, names):
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

View file

@ -12,7 +12,7 @@ class AddToLibraryAction(InterfaceAction):
name = 'Add To Library'
action_spec = (_('Add books to library'), 'add_book.png',
_('Add books to your calibre library from the connected device'), None)
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current'
def genesis(self):

View file

@ -18,6 +18,7 @@ class FetchAnnotationsAction(InterfaceAction):
name = 'Fetch Annotations'
action_spec = (_('Fetch annotations (experimental)'), None, None, None)
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current'
def genesis(self):

View file

@ -18,7 +18,7 @@ class GenerateCatalogAction(InterfaceAction):
name = 'Generate Catalog'
action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None)
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
def generate_catalog(self):
rows = self.gui.library_view.selectionModel().selectedRows()
@ -31,10 +31,10 @@ def generate_catalog(self):
_('No books selected for catalog generation'),
show=True)
db = self.gui.library_view.model().db
dbspec = {}
for id in ids:
dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)}
db = self.gui.library_view.model().db
dbspec = {}
for id in ids:
dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)}
# Calling gui2.tools:generate_catalog()
ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager,

View file

@ -8,7 +8,7 @@
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
@ -80,7 +80,7 @@ class ChooseLibraryAction(InterfaceAction):
name = 'Choose Library'
action_spec = (_('%d books'), 'lt.png',
_('Choose calibre library to work with'), None)
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
def genesis(self):
self.count_changed(0)
@ -88,6 +88,9 @@ def genesis(self):
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 @@ def genesis(self):
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 @@ def build_menus(self):
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()
@ -205,7 +210,6 @@ def build_menus(self):
rename_actions, delete_actions, qs_actions,
self.action_choose)
def location_selected(self, loc):
enabled = loc == 'library'
self.qaction.setEnabled(enabled)

View file

@ -20,7 +20,7 @@ class ConvertAction(InterfaceAction):
name = 'Convert Books'
action_spec = (_('Convert books'), 'convert.png', None, _('C'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current'
def genesis(self):

View file

@ -24,7 +24,7 @@ class ShareConnMenu(QMenu): # {{{
config_email = pyqtSignal()
toggle_server = pyqtSignal()
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
def __init__(self, parent=None):
QMenu.__init__(self, parent)
@ -121,8 +121,7 @@ class SendToDeviceAction(InterfaceAction):
name = 'Send To Device'
action_spec = (_('Send to device'), 'sync.png', None, _('D'))
dont_remove_from = frozenset(['toolbar-device'])
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
def genesis(self):
self.qaction.triggered.connect(self.do_sync)
@ -169,7 +168,7 @@ def content_server_state_changed(self, running):
def toggle_content_server(self):
if self.gui.content_server is None:
self.gui.start_content_server()
self.gui.start_content_server()
else:
self.gui.content_server.threaded_exit()
self.stopping_msg = info_dialog(self.gui, _('Stopping'),

View file

@ -12,7 +12,7 @@ class EditCollectionsAction(InterfaceAction):
name = 'Edit Collections'
action_spec = (_('Manage collections'), None,
_('Manage the collections on this device'), None)
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current'
def genesis(self):

View file

@ -141,15 +141,18 @@ def edit_metadata(self, checked, bulk=None):
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 @@ def do_edit_metadata_old(self, row_list, current_row):
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 @@ def do_edit_metadata(self, row_list, current_row):
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

View file

@ -11,7 +11,7 @@ class NextMatchAction(InterfaceAction):
name = 'Move to next highlighted book'
action_spec = (_('Move to next match'), 'arrow-down.png',
_('Move to next highlighted match'), [_('N'), _('F3')])
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current'
def genesis(self):

View file

@ -13,7 +13,7 @@ class OpenFolderAction(InterfaceAction):
name = 'Open Folder'
action_spec = (_('Open containing folder'), 'document_open.png', None,
_('O'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current'
def genesis(self):

View file

@ -16,7 +16,6 @@ class PreferencesAction(InterfaceAction):
name = 'Preferences'
action_spec = (_('Preferences'), 'config.png', None, _('Ctrl+P'))
dont_remove_from = frozenset(['toolbar'])
def genesis(self):
pm = QMenu()

View file

@ -15,7 +15,7 @@ class ShowBookDetailsAction(InterfaceAction):
name = 'Show Book Details'
action_spec = (_('Show book details'), 'dialog_information.png', None,
_('I'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current'
def genesis(self):

View file

@ -15,7 +15,7 @@ class TweakEpubAction(InterfaceAction):
action_spec = (_('Tweak ePub'), 'trim.png',
_('Make small changes to ePub format books'),
_('T'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current'
def genesis(self):

View file

@ -62,7 +62,7 @@ def setup_ui(self, parent):
w = self.widgets[1]
items = [_('Yes'), _('No'), _('Undefined')]
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
if not self.db.prefs.get('bools_are_tristate'):
items = items[:-1]
icons = icons[:-1]
for icon, text in zip(icons, items):
@ -70,7 +70,7 @@ def setup_ui(self, parent):
def setter(self, val):
val = {None: 2, False: 1, True: 0}[val]
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val == 2:
if not self.db.prefs.get('bools_are_tristate') and val == 2:
val = 1
self.widgets[1].setCurrentIndex(val)
@ -549,7 +549,7 @@ def get_initial_value(self, book_ids):
value = None
for book_id in book_ids:
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
if not self.db.prefs.get('bools_are_tristate') and val is None:
val = False
if value is not None and value != val:
return None
@ -559,7 +559,7 @@ def get_initial_value(self, book_ids):
def setup_ui(self, parent):
self.make_widgets(parent, QComboBox)
items = [_('Yes'), _('No')]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
if not self.db.prefs.get('bools_are_tristate'):
items.append('')
else:
items.append(_('Undefined'))
@ -571,7 +571,7 @@ def setup_ui(self, parent):
def getter(self):
val = self.main_widget.currentIndex()
if tweaks['bool_custom_columns_are_tristate'] == 'no':
if not self.db.prefs.get('bools_are_tristate'):
return {2: False, 1: False, 0: True}[val]
else:
return {2: None, 1: False, 0: True}[val]
@ -586,13 +586,13 @@ def commit(self, book_ids, notify=False):
return
val = self.gui_val
val = self.normalize_ui_val(val)
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
if not self.db.prefs.get('bools_are_tristate') and val is None:
val = False
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
def a_c_checkbox_changed(self):
if not self.ignore_change_signals:
if tweaks['bool_custom_columns_are_tristate'] == 'no' and \
if not self.db.prefs.get('bools_are_tristate') and \
self.main_widget.currentIndex() == 2:
self.a_c_checkbox.setChecked(False)
else:

View file

@ -7,12 +7,12 @@
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, QMenuBar, QAction,
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
from calibre.constants import __appname__
from calibre.constants import __appname__, isosx
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import gprefs
@ -238,6 +238,80 @@ def __init__(self, parent):
self.l.addStretch(10)
# }}}
class MenuAction(QAction):
def __init__(self, clone, parent):
QAction.__init__(self, clone.text(), parent)
self.clone = clone
clone.changed.connect(self.clone_changed)
def clone_changed(self):
self.setText(self.clone.text())
class MenuBar(QMenuBar): # {{{
def __init__(self, location_manager, parent):
QMenuBar.__init__(self, parent)
self.gui = parent
self.setNativeMenuBar(True)
self.location_manager = location_manager
self.location_manager.locations_changed.connect(self.build_bar)
self.added_actions = []
self.donate_action = QAction(_('Donate'), self)
self.donate_menu = QMenu()
self.donate_menu.addAction(self.gui.donate_action)
self.donate_action.setMenu(self.donate_menu)
self.build_bar()
def build_bar(self, changed_action=None):
showing_device = self.location_manager.has_device
actions = '-device' if showing_device else ''
actions = gprefs['action-layout-menubar'+actions]
show_main = len(actions) > 0
self.setVisible(show_main)
for ac in self.added_actions:
m = ac.menu()
if m is not None:
m.setVisible(False)
self.clear()
self.added_actions = []
self.action_map = {}
for what in actions:
if what is None:
continue
elif what == 'Location Manager':
for ac in self.location_manager.available_actions:
ac = self.build_menu(ac)
self.addAction(ac)
self.added_actions.append(ac)
elif what == 'Donate':
self.addAction(self.donate_action)
elif what in self.gui.iactions:
action = self.gui.iactions[what]
ac = self.build_menu(action.qaction)
self.addAction(ac)
self.added_actions.append(ac)
def build_menu(self, action):
m = action.menu()
ac = MenuAction(action, self)
if m is None:
m = QMenu()
m.addAction(action)
ac.setMenu(m)
return ac
# }}}
class ToolBar(QToolBar): # {{{
def __init__(self, donate, location_manager, child_bar, parent):
@ -284,6 +358,8 @@ def build_bar(self):
mactions = gprefs['action-layout-toolbar'+mactions]
cactions = gprefs['action-layout-toolbar-child']
show_main = len(mactions) > 0
self.setVisible(show_main)
show_child = len(cactions) > 0
self.child_bar.setVisible(show_child)
@ -306,11 +382,16 @@ def build_bar(self):
bar.added_actions.append(ac)
bar.setup_tool_button(bar, ac, QToolButton.MenuButtonPopup)
elif what == 'Donate':
self.d_widget = QWidget()
self.d_widget.setLayout(QVBoxLayout())
self.d_widget.layout().addWidget(self.donate_button)
bar.addWidget(self.d_widget)
self.showing_donate = True
if isosx:
bar.addAction(self.gui.donate_action)
ch = self.setup_tool_button(bar, self.gui.donate_action)
ch.setText(_('Donate'))
else:
self.d_widget = QWidget()
self.d_widget.setLayout(QVBoxLayout())
self.d_widget.layout().addWidget(self.donate_button)
bar.addWidget(self.d_widget)
self.showing_donate = True
elif what in self.gui.iactions:
action = self.gui.iactions[what]
bar.addAction(action.qaction)
@ -325,17 +406,20 @@ def setup_tool_button(self, bar, ac, menu_mode=None):
ch.setAutoRaise(True)
if ac.menu() is not None and menu_mode is not None:
ch.setPopupMode(menu_mode)
return ch
def resizeEvent(self, ev):
QToolBar.resizeEvent(self, ev)
style = Qt.ToolButtonTextUnderIcon
p = gprefs['toolbar_text']
if p == 'never':
style = Qt.ToolButtonIconOnly
s = gprefs['toolbar_icon_size']
if s != 'off':
p = gprefs['toolbar_text']
if p == 'never':
style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35 and \
not gprefs['action-layout-toolbar-child']:
style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35 and \
not gprefs['action-layout-toolbar-child']:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
@ -421,6 +505,9 @@ def __init__(self, db):
self.location_manager, self.child_bar, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
self.addToolBar(Qt.BottomToolBarArea, self.child_bar)
self.menu_bar = MenuBar(self.location_manager, self)
self.setMenuBar(self.menu_bar)
self.setUnifiedTitleAndToolBarOnMac(True)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)

View file

@ -353,7 +353,7 @@ def createEditor(self, parent, option, index):
editor = DelegateCB(parent)
items = [_('Y'), _('N'), ' ']
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
if not index.model().db.prefs.get('bools_are_tristate'):
items = items[:-1]
icons = icons[:-1]
for icon, text in zip(icons, items):
@ -367,7 +367,7 @@ def setModelData(self, editor, model, index):
def setEditorData(self, editor, index):
m = index.model()
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
if not m.db.prefs.get('bools_are_tristate'):
val = 1 if not val else 0
else:
val = 2 if val is None else 1 if not val else 0

View file

@ -700,7 +700,7 @@ def number_type(r, idx=-1):
self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx,
bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no')
self.db.prefs.get('bools_are_tristate'))
elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime':
@ -710,7 +710,7 @@ def number_type(r, idx=-1):
self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx,
bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no')
self.db.prefs.get('bools_are_tristate'))
elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx)
elif datatype == 'series':

View file

@ -521,7 +521,7 @@ def create_row2(row, widget, button=None):
# }}}
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
@ -654,10 +654,14 @@ def create_row(row, widget, tab_to, button=None, icon=None, span=1):
# }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1}
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
set_current_callback=None):
d = MetadataSingleDialog(db, parent)
cls = db.prefs.get('edit_metadata_single_layout', '')
if cls not in editors:
cls = 'default'
d = editors[cls](db, parent)
d.start(row_list, current_row, view_slot=view_slot,
set_current_callback=set_current_callback)
return d.changed, d.rows_to_refresh

View file

@ -8,6 +8,7 @@
__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,
@ -22,7 +23,7 @@
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): # {{{
@ -51,7 +52,7 @@ def paint(self, painter, option, index):
painter.restore()
# }}}
class ResultsModel(QAbstractTableModel):
class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
@ -71,15 +72,12 @@ 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:
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:
@ -110,10 +108,33 @@ def data(self, index, role):
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)
@ -121,6 +142,9 @@ def __init__(self, parent=None):
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)
@ -129,6 +153,38 @@ def show_results(self, results):
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 = [
'<center>',
'<h2>%s</h2>'%book.title,
'<div><i>%s</i></div>'%authors_to_string(book.authors),
]
if not book.is_null('rating'):
parts.append('<div>%s</div>'%('\u2605'*int(book.rating)))
parts.append('</center>')
if book.tags:
parts.append('<div>%s</div><div>\u00a0</div>'%', '.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())
# }}}
@ -227,6 +283,8 @@ def run(self):
class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal()
results_found = pyqtSignal()
book_selected = pyqtSignal(object)
def __init__(self, log, parent=None):
QWidget.__init__(self, parent)
@ -244,11 +302,15 @@ def __init__(self, log, parent=None):
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)
@ -324,6 +386,13 @@ def process_results(self):
self.results_view.show_results(self.worker.results)
self.comments_view.show_data('''
<div style="margin-bottom:2ex">Found <b>%d</b> results</div>
<div>To see <b>details</b>, click on any result</div>''' %
len(self.worker.results))
self.results_found.emit()
def cancel(self):
self.abort.set()
@ -343,23 +412,46 @@ def __init__(self, log, parent=None):
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={}):
self.identify_widget.start(title=title, authors=authors,
identifiers=identifiers)

View file

@ -13,6 +13,7 @@
from calibre.gui2.preferences.columns_ui import Ui_Form
from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS
from calibre.utils.config import test_eight_code
class ConfigWidget(ConfigWidgetBase, Ui_Form):
@ -33,6 +34,14 @@ def genesis(self, gui):
signal = getattr(self.opt_columns, 'item'+signal)
signal.connect(self.columns_changed)
if test_eight_code:
r = self.register
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
r('edit_metadata_single_layout', db.prefs, choices=choices)
r('bools_are_tristate', db.prefs, restart_required=True)
else:
self.items_in_v_eight.setVisible(False)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.init_columns()
@ -169,6 +178,10 @@ def col_pos(x, y):
must_restart = True
return must_restart
def refresh_gui(self, gui):
gui.library_view.reset()
if __name__ == '__main__':
from PyQt4.Qt import QApplication

View file

@ -197,6 +197,67 @@
</property>
</widget>
</item>
<item row="1" column="2">
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox" name="items_in_v_eight">
<property name="title">
<string>Related Options</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel">
<property name="text">
<string>Edit metadata layout:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_single_layout</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_edit_metadata_single_layout">
<property name="toolTip">
<string>Choose a different layout for the Edit Metadata dialog. Alternate layouts make it easier to edit custom columns.</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>Boolean columns are tristate:</string>
</property>
<property name="buddy">
<cstring>opt_bools_are_tristate</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="opt_bools_are_tristate">
<property name="toolTip">
<string>If checked, boolean columns values can be Yes, No, and Unknown.
If not checked, the values can be Yes and No.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources>

View file

@ -360,6 +360,7 @@ def closeEvent(self, *args):
self.gui.create_device_menu()
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
self.gui.tool_bar.build_bar()
self.gui.menu_bar.build_bar()
self.gui.build_context_menus()
self.gui.tool_bar.apply_settings()

View file

@ -34,9 +34,12 @@ def name_to_action(self, name, gui):
if name == 'Location Manager':
return FakeAction(name, None,
_('Switch between library and device views'),
dont_remove_from=set(['toolbar-device']))
dont_add_to=frozenset(['menubar', 'toolbar',
'toolbar-child', 'context-menu',
'context-menu-device']))
if name is None:
return FakeAction('--- '+_('Separator')+' ---', None)
return FakeAction('--- '+_('Separator')+' ---', None,
dont_add_to=frozenset(['menubar', 'menubar-device']))
try:
return gui.iactions[name]
except:
@ -89,7 +92,7 @@ def __init__(self, key, gui):
self._data = self.get_all_actions(current)
def get_all_actions(self, current):
all = list(self.gui.iactions.keys()) + ['Donate']
all = list(self.gui.iactions.keys()) + ['Donate', 'Location Manager']
all = [x for x in all if x not in current] + [None]
all = [self.name_to_action(x, self.gui) for x in all]
all = [x for x in all if self.key not in x.dont_add_to]
@ -208,12 +211,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
LOCATIONS = [
('toolbar', _('The main toolbar')),
('toolbar-child', _('The optional second toolbar')),
('toolbar-device', _('The main toolbar when a device is connected')),
('toolbar-child', _('The optional second toolbar')),
('menubar', _('The menubar')),
('menubar-device', _('The menubar when a device is connected')),
('context-menu', _('The context menu for the books in the '
'calibre library')),
('context-menu-device', _('The context menu for the books on '
'the device'))
'the device')),
]
def genesis(self, gui):
@ -284,6 +289,21 @@ def move(self, delta, *args):
self.changed_signal.emit()
def commit(self):
# Ensure preferences are showing in either the toolbar or
# the menubar.
pref_in_toolbar = lm_in_toolbar = False
cm = self.models['toolbar']
for x in cm[1]._data:
if x.name == 'Preferences':
pref_in_toolbar = True
if x.name == 'Location Manager':
lm_in_toolbar = True
if not pref_in_toolbar:
self.models['menubar'][1].add(['Preferences'])
if not lm_in_toolbar:
self.models['menubar-device'][1].add(['Location Manager'])
# Save data.
for am, cm in self.models.values():
cm.commit()
return False

View file

@ -153,6 +153,7 @@ def initialize(self, library_path, db, listener, actions, show_gui=True):
for ac in self.iactions.values():
ac.do_genesis()
self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self)
MainWindowMixin.__init__(self, db)
# Jobs Button {{{
@ -186,8 +187,7 @@ def initialize(self, library_path, db, listener, actions, show_gui=True):
self.system_tray_menu = QMenu(self)
self.restore_action = self.system_tray_menu.addAction(
QIcon(I('page.png')), _('&Restore'))
self.donate_action = self.system_tray_menu.addAction(
QIcon(I('donate.png')), _('&Donate to support calibre'))
self.system_tray_menu.addAction(self.donate_action)
self.donate_button.setDefaultAction(self.donate_action)
self.donate_button.setStatusTip(self.donate_button.toolTip())
self.eject_action = self.system_tray_menu.addAction(

View file

@ -547,7 +547,7 @@ def _matchkind(self, query):
return matchkind, query
def get_bool_matches(self, location, query, candidates):
bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] != 'no'
bools_are_tristate = not self.db_prefs.get('bools_are_tristate')
loc = self.field_metadata[location]['rec_index']
matches = set()
query = icu_lower(query)
@ -947,7 +947,7 @@ def multisort(self, fields=[], subsort=False):
if not fields:
fields = [('timestamp', False)]
keyg = SortKeyGenerator(fields, self.field_metadata, self._data)
keyg = SortKeyGenerator(fields, self.field_metadata, self._data, self.db_prefs)
self._map.sort(key=keyg)
tmap = list(itertools.repeat(False, len(self._data)))
@ -970,9 +970,10 @@ def __cmp__(self, other):
class SortKeyGenerator(object):
def __init__(self, fields, field_metadata, data):
def __init__(self, fields, field_metadata, data, db_prefs):
from calibre.utils.icu import sort_key
self.field_metadata = field_metadata
self.db_prefs = db_prefs
self.orders = [1 if x[1] else -1 for x in fields]
self.entries = [(x[0], field_metadata[x[0]]) for x in fields]
self.library_order = tweaks['title_series_sorting'] == 'library_order'
@ -1032,7 +1033,7 @@ def itervals(self, record):
val = self.string_sort_key(val)
elif dt == 'bool':
if tweaks['bool_custom_columns_are_tristate'] == 'no':
if not self.db_prefs.get('bools_are_tristate'):
val = {True: 1, False: 2, None: 2}.get(val, 2)
else:
val = {True: 1, False: 2, None: 3}.get(val, 3)

View file

@ -40,7 +40,7 @@
from calibre.utils.magick.draw import save_cover_data_to
from calibre.utils.recycle_bin import delete_file, delete_tree
from calibre.utils.formatter_functions import load_user_template_functions
from calibre.utils.config import test_eight_code
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
@ -213,6 +213,12 @@ def initialize_dynamic(self):
defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = ''
defs['categories_using_hierarchy'] = []
defs['edit_metadata_single_layout'] = 'default'
defs['bools_are_tristate'] = \
tweaks.get('bool_custom_columns_are_tristate', 'yes') == 'yes'
if self.prefs.get('bools_are_tristate') is None or not test_eight_code:
self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
# Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default):

View file

@ -17,8 +17,8 @@
class CSSortKeyGenerator(SortKeyGenerator):
def __init__(self, fields, fm):
SortKeyGenerator.__init__(self, fields, fm, None)
def __init__(self, fields, fm, db_prefs):
SortKeyGenerator.__init__(self, fields, fm, None, db_prefs)
def __call__(self, record):
return self.itervals(record).next()
@ -56,7 +56,8 @@ def sort(self, items, field, order):
field = self.db.data.sanitize_sort_field_name(field)
if field not in self.db.field_metadata.sortable_field_keys():
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata)
keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata,
self.db.prefs)
items.sort(key=keyg, reverse=not order)
# }}}

View file

@ -197,7 +197,7 @@ Once you've located the zip file of your plugin you can then directly update it
zip -R /path/to/plugin/zip/file.zip *
This will automatically update all changed files. It relies on the freely available zip command line tool.
This will update all changed files. It relies on the freely available zip command line tool. Note that you should quit calibre before running this command.
More plugin examples
----------------------

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -785,6 +785,9 @@ def write_tweaks(raw):
tweaks = read_tweaks()
test_eight_code = tweaks.get('test_eight_code', False)
# test_eight_code notes
# Change documentation of bool columns are tristate to indicate that it can be
# overridden on a per library basis via Preferences->Custom columns
def migrate():
if hasattr(os, 'geteuid') and os.geteuid() == 0: