KG updates

This commit is contained in:
GRiker 2011-06-23 10:12:58 -06:00
commit 9bc3676dbc
18 changed files with 1126 additions and 642 deletions

View File

@ -261,7 +261,7 @@ class OutputFormatPlugin(Plugin):
@property @property
def description(self): def description(self):
return _('Convert ebooks to the %s format'%self.file_type) return _('Convert ebooks to the %s format')%self.file_type
def __init__(self, *args): def __init__(self, *args):
Plugin.__init__(self, *args) Plugin.__init__(self, *args)

View File

@ -125,9 +125,9 @@ class KOBO(USBMS):
# this shows an expired Collection so the user can decide to delete the book # this shows an expired Collection so the user can decide to delete the book
if expired == 3: if expired == 3:
playlist_map[lpath].append('Expired') playlist_map[lpath].append('Expired')
# Favourites are supported on the touch but the data field is there on most earlier models # A SHORTLIST is supported on the touch but the data field is there on most earlier models
if favouritesindex == 1: if favouritesindex == 1:
playlist_map[lpath].append('Favourite') playlist_map[lpath].append('Shortlist')
path = self.normalize_path(path) path = self.normalize_path(path)
# print "Normalized FileName: " + path # print "Normalized FileName: " + path
@ -557,6 +557,7 @@ class KOBO(USBMS):
if collections: if collections:
# Process any collections that exist # Process any collections that exist
for category, books in collections.items(): for category, books in collections.items():
# debug_print (category)
if category == 'Im_Reading': if category == 'Im_Reading':
# Reset Im_Reading list in the database # Reset Im_Reading list in the database
if oncard == 'carda': if oncard == 'carda':
@ -575,7 +576,8 @@ class KOBO(USBMS):
for book in books: for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path) # debug_print('Title:', book.title, 'lpath:', book.path)
book.device_collections = ['Im_Reading'] if 'Im_Reading' not in book.device_collections:
book.device_collections.append('Im_Reading')
extension = os.path.splitext(book.path)[1] extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
@ -618,7 +620,8 @@ class KOBO(USBMS):
for book in books: for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path) # debug_print('Title:', book.title, 'lpath:', book.path)
book.device_collections = ['Read'] if 'Read' not in book.device_collections:
book.device_collections.append('Read')
extension = os.path.splitext(book.path)[1] extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
@ -654,7 +657,8 @@ class KOBO(USBMS):
for book in books: for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path) # debug_print('Title:', book.title, 'lpath:', book.path)
book.device_collections = ['Closed'] if 'Closed' not in book.device_collections:
book.device_collections.append('Closed')
extension = os.path.splitext(book.path)[1] extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
@ -672,6 +676,44 @@ class KOBO(USBMS):
else: else:
connection.commit() connection.commit()
# debug_print('Database: Commit set ReadStatus as Closed') # debug_print('Database: Commit set ReadStatus as Closed')
if category == 'Shortlist':
# Reset FavouritesIndex list in the database
if oncard == 'carda':
query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID like \'file:///mnt/sd/%\''
elif oncard != 'carda' and oncard != 'cardb':
query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID not like \'file:///mnt/sd/%\''
try:
cursor.execute (query)
except:
debug_print('Database Exception: Unable to reset Shortlist list')
raise
else:
# debug_print('Commit: Reset Shortlist list')
connection.commit()
for book in books:
# debug_print('Title:', book.title, 'lpath:', book.path)
if 'Shortlist' not in book.device_collections:
book.device_collections.append('Shortlist')
# debug_print ("Shortlist found for: ", book.title)
extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
ContentID = self.contentid_from_path(book.path, ContentType)
# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
t = (ContentID,)
try:
cursor.execute('update content set FavouritesIndex=1 where BookID is Null and ContentID = ?', t)
except:
debug_print('Database Exception: Unable set book as Shortlist')
raise
else:
connection.commit()
# debug_print('Database: Commit set Shortlist as Shortlist')
else: # No collections else: # No collections
# Since no collections exist the ReadStatus needs to be reset to 0 (Unread) # Since no collections exist the ReadStatus needs to be reset to 0 (Unread)
print "Reseting ReadStatus to 0" print "Reseting ReadStatus to 0"

View File

@ -19,8 +19,9 @@ class TECLAST_K3(USBMS):
PRODUCT_ID = [0x3203] PRODUCT_ID = [0x3203]
BCD = [0x0000, 0x0100] BCD = [0x0000, 0x0100]
VENDOR_NAME = 'TECLAST' VENDOR_NAME = ['TECLAST', 'IMAGIN']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['DIGITAL_PLAYER', 'TL-K5',
'EREADER']
MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'K3 Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card' STORAGE_CARD_VOLUME_LABEL = 'K3 Storage Card'

View File

@ -13,8 +13,7 @@ from calibre.gui2 import error_dialog
class ShowQuickviewAction(InterfaceAction): class ShowQuickviewAction(InterfaceAction):
name = 'Show quickview' name = 'Show quickview'
action_spec = (_('Show quickview'), 'user_profile.png', None, action_spec = (_('Show quickview'), 'search.png', None, _('Q'))
_('Q'))
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device']) dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current' action_type = 'current'

View File

@ -451,7 +451,8 @@ class Saver(QObject): # {{{
self.callback_called = False self.callback_called = False
self.rq = Queue() self.rq = Queue()
self.ids = [x for x in map(db.id, [r.row() for r in rows]) if x is not None] self.ids = [x for x in map(db.id, [r.row() for r in rows]) if x is not None]
self.pd.set_max(len(self.ids)) self.pd_max = len(self.ids)
self.pd.set_max(0)
self.pd.value = 0 self.pd.value = 0
self.failures = set([]) self.failures = set([])
@ -510,6 +511,8 @@ class Saver(QObject): # {{{
id, title, ok, tb = self.rq.get_nowait() id, title, ok, tb = self.rq.get_nowait()
except Empty: except Empty:
return return
if self.pd.max != self.pd_max:
self.pd.max = self.pd_max
self.pd.value += 1 self.pd.value += 1
self.ids.remove(id) self.ids.remove(id)
if not isinstance(title, unicode): if not isinstance(title, unicode):

View File

@ -25,7 +25,7 @@ class Base(object):
def __init__(self, db, col_id, parent=None): def __init__(self, db, col_id, parent=None):
self.db, self.col_id = db, col_id self.db, self.col_id = db, col_id
self.col_metadata = db.custom_column_num_map[col_id] self.col_metadata = db.custom_column_num_map[col_id]
self.initial_val = None self.initial_val = self.widgets = None
self.setup_ui(parent) self.setup_ui(parent)
def initialize(self, book_id): def initialize(self, book_id):
@ -54,6 +54,9 @@ class Base(object):
def normalize_ui_val(self, val): def normalize_ui_val(self, val):
return val return val
def break_cycles(self):
self.db = self.widgets = self.initial_val = None
class Bool(Base): class Bool(Base):
def setup_ui(self, parent): def setup_ui(self, parent):

View File

@ -53,6 +53,13 @@ class ProgressDialog(QDialog, Ui_Dialog):
def set_max(self, max): def set_max(self, max):
self.bar.setMaximum(max) self.bar.setMaximum(max)
@dynamic_property
def max(self):
def fget(self): return self.bar.maximum()
def fset(self, val): self.bar.setMaximum(val)
return property(fget=fget, fset=fset)
def _canceled(self, *args): def _canceled(self, *args):
self.canceled = True self.canceled = True
self.button_box.setDisabled(True) self.button_box.setDisabled(True)

View File

@ -5,23 +5,27 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import (Qt, QDialog, QAbstractItemView, QTableWidgetItem, from PyQt4.Qt import (Qt, QDialog, QAbstractItemView, QTableWidgetItem,
QListWidgetItem, QByteArray, QModelIndex) QListWidgetItem, QByteArray, QModelIndex, QCoreApplication)
from calibre.gui2.dialogs.quickview_ui import Ui_Quickview from calibre.gui2.dialogs.quickview_ui import Ui_Quickview
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
class tableItem(QTableWidgetItem): class TableItem(QTableWidgetItem):
'''
A QTableWidgetItem that sorts on a separate string and uses ICU rules
'''
def __init__(self, val): def __init__(self, val, sort):
self.sort = sort
QTableWidgetItem.__init__(self, val) QTableWidgetItem.__init__(self, val)
self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) self.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
def __ge__(self, other): def __ge__(self, other):
return sort_key(unicode(self.text())) >= sort_key(unicode(other.text())) return sort_key(self.sort) >= sort_key(other.sort)
def __lt__(self, other): def __lt__(self, other):
return sort_key(unicode(self.text())) < sort_key(unicode(other.text())) return sort_key(self.sort) < sort_key(other.sort)
class Quickview(QDialog, Ui_Quickview): class Quickview(QDialog, Ui_Quickview):
@ -31,12 +35,16 @@ class Quickview(QDialog, Ui_Quickview):
self.setupUi(self) self.setupUi(self)
self.isClosed = False self.isClosed = False
self.books_table_column_widths = None
try: try:
self.books_table_column_widths = \
gprefs.get('quickview_dialog_books_table_widths', None)
geom = gprefs.get('quickview_dialog_geometry', bytearray('')) geom = gprefs.get('quickview_dialog_geometry', bytearray(''))
self.restoreGeometry(QByteArray(geom)) self.restoreGeometry(QByteArray(geom))
except: except:
pass pass
# Remove the help button from the window title bar
icon = self.windowIcon() icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon) self.setWindowIcon(icon)
@ -44,11 +52,16 @@ class Quickview(QDialog, Ui_Quickview):
self.db = view.model().db self.db = view.model().db
self.view = view self.view = view
self.gui = gui self.gui = gui
self.is_closed = False
self.current_book_id = None
self.current_key = None
self.use_current_key_for_next_refresh = False
self.last_search = None
self.items.setSelectionMode(QAbstractItemView.SingleSelection) self.items.setSelectionMode(QAbstractItemView.SingleSelection)
self.items.currentTextChanged.connect(self.item_selected) self.items.currentTextChanged.connect(self.item_selected)
# self.items.setFixedWidth(150)
# Set up the books table columns
self.books_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.books_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.books_table.setSelectionMode(QAbstractItemView.SingleSelection) self.books_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.books_table.setColumnCount(3) self.books_table.setColumnCount(3)
@ -60,67 +73,81 @@ class Quickview(QDialog, Ui_Quickview):
self.books_table.setHorizontalHeaderItem(2, t) self.books_table.setHorizontalHeaderItem(2, t)
self.books_table_header_height = self.books_table.height() self.books_table_header_height = self.books_table.height()
self.books_table.cellDoubleClicked.connect(self.book_doubleclicked) self.books_table.cellDoubleClicked.connect(self.book_doubleclicked)
self.books_table.sortByColumn(0, Qt.AscendingOrder)
self.is_closed = False # get the standard table row height. Do this here because calling
self.current_book_id = None # resizeRowsToContents can word wrap long cell contents, creating
self.current_key = None # double-high rows
self.use_current_key_for_next_refresh = False self.books_table.setRowCount(1)
self.last_search = None self.books_table.setItem(0, 0, TableItem('A', ''))
self.books_table.resizeRowsToContents()
self.books_table_row_height = self.books_table.rowHeight(0)
self.books_table.setRowCount(0)
# Add the data
self.refresh(row) self.refresh(row)
# self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
self.view.selectionModel().currentChanged[QModelIndex,QModelIndex].connect(self.slave) self.view.selectionModel().currentChanged[QModelIndex,QModelIndex].connect(self.slave)
QCoreApplication.instance().aboutToQuit.connect(self.save_state)
self.search_button.clicked.connect(self.do_search) self.search_button.clicked.connect(self.do_search)
# search button
def do_search(self): def do_search(self):
if self.last_search is not None: if self.last_search is not None:
self.use_current_key_for_next_refresh = True self.use_current_key_for_next_refresh = True
self.gui.search.set_search_string(self.last_search) self.gui.search.set_search_string(self.last_search)
# clicks on the items listWidget
def item_selected(self, txt): def item_selected(self, txt):
self.fill_in_books_box(unicode(txt)) self.fill_in_books_box(unicode(txt))
# Given a cell in the library view, display the information
def refresh(self, idx): def refresh(self, idx):
bv_row = idx.row() bv_row = idx.row()
key = self.view.model().column_map[idx.column()] key = self.view.model().column_map[idx.column()]
book_id = self.view.model().id(bv_row) book_id = self.view.model().id(bv_row)
# Double-clicking on a book to show it in the library view will result
# in a signal emitted for column 1 of the book row. Use the original
# column for this signal.
if self.use_current_key_for_next_refresh: if self.use_current_key_for_next_refresh:
key = self.current_key key = self.current_key
self.use_current_key_for_next_refresh = False self.use_current_key_for_next_refresh = False
else: else:
# Only show items for categories
if not self.db.field_metadata[key]['is_category']: if not self.db.field_metadata[key]['is_category']:
if self.current_key is None: if self.current_key is None:
return return
key = self.current_key key = self.current_key
self.items_label.setText('{0} ({1})'.format( self.items_label.setText('{0} ({1})'.format(
self.db.field_metadata[key]['name'], key)) self.db.field_metadata[key]['name'], key))
self.items.blockSignals(True)
self.items.clear() self.items.clear()
self.books_table.setRowCount(0) self.books_table.setRowCount(0)
mi = self.db.get_metadata(book_id, index_is_id=True, get_user_categories=False) mi = self.db.get_metadata(book_id, index_is_id=True, get_user_categories=False)
vals = mi.get(key, None) vals = mi.get(key, None)
if not vals:
return
if vals:
if not isinstance(vals, list): if not isinstance(vals, list):
vals = [vals] vals = [vals]
vals.sort(key=sort_key) vals.sort(key=sort_key)
self.items.blockSignals(True)
for v in vals: for v in vals:
a = QListWidgetItem(v) a = QListWidgetItem(v)
self.items.addItem(a) self.items.addItem(a)
self.items.setCurrentRow(0) self.items.setCurrentRow(0)
self.items.blockSignals(False)
self.current_book_id = book_id self.current_book_id = book_id
self.current_key = key self.current_key = key
self.fill_in_books_box(vals[0]) self.fill_in_books_box(vals[0])
self.items.blockSignals(False)
def fill_in_books_box(self, selected_item): def fill_in_books_box(self, selected_item):
# Do a bit of fix-up on the items so that the search works.
if selected_item.startswith('.'): if selected_item.startswith('.'):
sv = '.' + selected_item sv = '.' + selected_item
else: else:
@ -129,43 +156,82 @@ class Quickview(QDialog, Ui_Quickview):
self.last_search = self.current_key+':"=' + sv + '"' self.last_search = self.current_key+':"=' + sv + '"'
books = self.db.search_getting_ids(self.last_search, books = self.db.search_getting_ids(self.last_search,
self.db.data.search_restriction) self.db.data.search_restriction)
self.books_table.setRowCount(len(books)) self.books_table.setRowCount(len(books))
self.books_label.setText(_('Books with selected item: {0}').format(len(books))) self.books_label.setText(_('Books with selected item: {0}').format(len(books)))
select_row = None select_item = None
self.books_table.setSortingEnabled(False) self.books_table.setSortingEnabled(False)
for row, b in enumerate(books): for row, b in enumerate(books):
mi = self.db.get_metadata(b, index_is_id=True, get_user_categories=False) mi = self.db.get_metadata(b, index_is_id=True, get_user_categories=False)
a = tableItem(mi.title) a = TableItem(mi.title, mi.title_sort)
a.setData(Qt.UserRole, b) a.setData(Qt.UserRole, b)
self.books_table.setItem(row, 0, a) self.books_table.setItem(row, 0, a)
a = tableItem(' & '.join(mi.authors)) if b == self.current_book_id:
select_item = a
a = TableItem(' & '.join(mi.authors), mi.author_sort)
self.books_table.setItem(row, 1, a) self.books_table.setItem(row, 1, a)
series = mi.format_field('series')[1] series = mi.format_field('series')[1]
if series is None: if series is None:
series = '' series = ''
a = tableItem(series) a = TableItem(series, series)
self.books_table.setItem(row, 2, a) self.books_table.setItem(row, 2, a)
if b == self.current_book_id: self.books_table.setRowHeight(row, self.books_table_row_height)
select_row = row
self.books_table.resizeColumnsToContents()
# self.books_table.resizeRowsToContents()
if select_row is not None:
self.books_table.selectRow(select_row)
self.books_table.setSortingEnabled(True) self.books_table.setSortingEnabled(True)
if select_item is not None:
self.books_table.setCurrentItem(select_item)
self.books_table.scrollToItem(select_item, QAbstractItemView.PositionAtCenter)
# Deal with sizing the table columns. Done here because the numbers are not
# correct until the first paint.
def resizeEvent(self, *args):
QDialog.resizeEvent(self, *args)
if self.books_table_column_widths is not None:
for c,w in enumerate(self.books_table_column_widths):
self.books_table.setColumnWidth(c, w)
else:
# the vertical scroll bar might not be rendered, so might not yet
# have a width. Assume 25. Not a problem because user-changed column
# widths will be remembered
w = self.books_table.width() - 25 - self.books_table.verticalHeader().width()
w /= self.books_table.columnCount()
for c in range(0, self.books_table.columnCount()):
self.books_table.setColumnWidth(c, w)
self.save_state()
def book_doubleclicked(self, row, column): def book_doubleclicked(self, row, column):
self.use_current_key_for_next_refresh = True self.use_current_key_for_next_refresh = True
self.view.select_rows([self.books_table.item(row, 0).data(Qt.UserRole).toInt()[0]]) self.view.select_rows([self.books_table.item(row, 0).data(Qt.UserRole).toInt()[0]])
# called when a book is clicked on the library view
def slave(self, current, previous): def slave(self, current, previous):
if self.is_closed:
return
self.refresh(current) self.refresh(current)
self.view.activateWindow() self.view.activateWindow()
def done(self, r): def save_state(self):
geom = bytearray(self.saveGeometry()) if self.is_closed:
gprefs['quickview_dialog_geometry'] = geom return
self.books_table_column_widths = []
for c in range(0, self.books_table.columnCount()):
self.books_table_column_widths.append(self.books_table.columnWidth(c))
gprefs['quickview_dialog_books_table_widths'] = self.books_table_column_widths
gprefs['quickview_dialog_geometry'] = bytearray(self.saveGeometry())
def close(self):
self.save_state()
# clean up to prevent memory leaks
self.db = self.view = self.gui = None
self.is_closed = True self.is_closed = True
QDialog.done(self, r)
# called by the window system
def closeEvent(self, *args):
self.close()
QDialog.closeEvent(self, *args)
# called by the close button
def reject(self):
self.close()
QDialog.reject(self)

View File

@ -21,9 +21,10 @@ from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import (title_sort, authors_to_string, from calibre.ebooks.metadata import (title_sort, authors_to_string,
string_to_authors, check_isbn, authors_to_sort_string) string_to_authors, check_isbn, authors_to_sort_string)
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE,
choose_files, error_dialog, choose_images) choose_files, error_dialog, choose_images)
from calibre.utils.date import local_tz, qt_to_dt from calibre.utils.date import (local_tz, qt_to_dt, as_local_time,
UNDEFINED_DATE)
from calibre import strftime from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
@ -125,6 +126,9 @@ class TitleEdit(EnLineEdit):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def break_cycles(self):
self.dialog = None
class TitleSortEdit(TitleEdit): class TitleSortEdit(TitleEdit):
TITLE_ATTR = 'title_sort' TITLE_ATTR = 'title_sort'
@ -150,6 +154,7 @@ class TitleSortEdit(TitleEdit):
self.title_edit.textChanged.connect(self.update_state) self.title_edit.textChanged.connect(self.update_state)
self.textChanged.connect(self.update_state) self.textChanged.connect(self.update_state)
self.autogen_button = autogen_button
autogen_button.clicked.connect(self.auto_generate) autogen_button.clicked.connect(self.auto_generate)
self.update_state() self.update_state()
@ -168,6 +173,9 @@ class TitleSortEdit(TitleEdit):
def auto_generate(self, *args): def auto_generate(self, *args):
self.current_val = title_sort(self.title_edit.current_val) self.current_val = title_sort(self.title_edit.current_val)
self.title_edit.textChanged.disconnect()
self.textChanged.disconnect()
self.autogen_button.clicked.disconnect()
# }}} # }}}
@ -185,6 +193,7 @@ class AuthorsEdit(MultiCompleteComboBox):
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
self.setEditable(True) self.setEditable(True)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.manage_authors_signal = manage_authors
manage_authors.triggered.connect(self.manage_authors) manage_authors.triggered.connect(self.manage_authors)
def manage_authors(self): def manage_authors(self):
@ -269,6 +278,10 @@ class AuthorsEdit(MultiCompleteComboBox):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def break_cycles(self):
self.db = self.dialog = None
self.manage_authors_signal.triggered.disconnect()
class AuthorSortEdit(EnLineEdit): class AuthorSortEdit(EnLineEdit):
TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
@ -297,6 +310,10 @@ class AuthorSortEdit(EnLineEdit):
self.authors_edit.editTextChanged.connect(self.update_state_and_val) self.authors_edit.editTextChanged.connect(self.update_state_and_val)
self.textChanged.connect(self.update_state) self.textChanged.connect(self.update_state)
self.autogen_button = autogen_button
self.copy_a_to_as_action = copy_a_to_as_action
self.copy_as_to_a_action = copy_as_to_a_action
autogen_button.clicked.connect(self.auto_generate) autogen_button.clicked.connect(self.auto_generate)
copy_a_to_as_action.triggered.connect(self.auto_generate) copy_a_to_as_action.triggered.connect(self.auto_generate)
copy_as_to_a_action.triggered.connect(self.copy_to_authors) copy_as_to_a_action.triggered.connect(self.copy_to_authors)
@ -368,6 +385,15 @@ class AuthorSortEdit(EnLineEdit):
db.set_author_sort(id_, aus, notify=False, commit=False) db.set_author_sort(id_, aus, notify=False, commit=False)
return True return True
def break_cycles(self):
self.db = None
self.authors_edit.editTextChanged.disconnect()
self.textChanged.disconnect()
self.autogen_button.clicked.disconnect()
self.copy_a_to_as_action.triggered.disconnect()
self.copy_as_to_a_action.triggered.disconnect()
self.authors_edit = None
# }}} # }}}
# Series {{{ # Series {{{
@ -427,6 +453,10 @@ class SeriesEdit(MultiCompleteComboBox):
commit=True, allow_case_change=True) commit=True, allow_case_change=True)
return True return True
def break_cycles(self):
self.dialog = None
class SeriesIndexEdit(QDoubleSpinBox): class SeriesIndexEdit(QDoubleSpinBox):
TOOLTIP = '' TOOLTIP = ''
@ -488,6 +518,11 @@ class SeriesIndexEdit(QDoubleSpinBox):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def break_cycles(self):
self.series_edit.currentIndexChanged.disconnect()
self.series_edit.editTextChanged.disconnect()
self.series_edit.lineEdit().editingFinished.disconnect()
self.db = self.series_edit = self.dialog = None
# }}} # }}}
@ -699,6 +734,8 @@ class FormatsManager(QWidget): # {{{
if old != prefs['read_file_metadata']: if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old prefs['read_file_metadata'] = old
def break_cycles(self):
self.dialog = None
# }}} # }}}
class Cover(ImageView): # {{{ class Cover(ImageView): # {{{
@ -860,6 +897,10 @@ class Cover(ImageView): # {{{
db.remove_cover(id_, notify=False, commit=False) db.remove_cover(id_, notify=False, commit=False)
return True return True
def break_cycles(self):
self.cover_changed.disconnect()
self.dialog = self._cdata = self.current_val = self.original_val = None
# }}} # }}}
class CommentsEdit(Editor): # {{{ class CommentsEdit(Editor): # {{{
@ -1211,6 +1252,7 @@ class DateEdit(QDateEdit): # {{{
def fset(self, val): def fset(self, val):
if val is None: if val is None:
val = UNDEFINED_DATE val = UNDEFINED_DATE
val = as_local_time(val)
self.setDate(QDate(val.year, val.month, val.day)) self.setDate(QDate(val.year, val.month, val.day))
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)

View File

@ -481,6 +481,13 @@ class MetadataSingleDialogBase(ResizableDialog):
x = getattr(self, b, None) x = getattr(self, b, None)
if x is not None: if x is not None:
disconnect(x.clicked) disconnect(x.clicked)
for widget in self.basic_metadata_widgets:
bc = getattr(widget, 'break_cycles', None)
if bc is not None and callable(bc):
bc()
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.break_cycles()
# }}} # }}}
class Splitter(QSplitter): class Splitter(QSplitter):

View File

@ -72,19 +72,27 @@ class ConditionEditor(QWidget): # {{{
self.l = l = QGridLayout(self) self.l = l = QGridLayout(self)
self.setLayout(l) self.setLayout(l)
self.l1 = l1 = QLabel(_('If the ')) texts = _('If the ___ column ___ values')
try:
one, two, three = texts.split('___')
except:
one, two, three = 'If the ', ' column ', ' value '
self.l1 = l1 = QLabel(one)
l.addWidget(l1, 0, 0) l.addWidget(l1, 0, 0)
self.column_box = QComboBox(self) self.column_box = QComboBox(self)
l.addWidget(self.column_box, 0, 1) l.addWidget(self.column_box, 0, 1)
self.l2 = l2 = QLabel(_(' column '))
self.l2 = l2 = QLabel(two)
l.addWidget(l2, 0, 2) l.addWidget(l2, 0, 2)
self.action_box = QComboBox(self) self.action_box = QComboBox(self)
l.addWidget(self.action_box, 0, 3) l.addWidget(self.action_box, 0, 3)
self.l3 = l3 = QLabel(_(' value ')) self.l3 = l3 = QLabel(three)
l.addWidget(l3, 0, 4) l.addWidget(l3, 0, 4)
self.value_box = QLineEdit(self) self.value_box = QLineEdit(self)

View File

@ -10,6 +10,7 @@ from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting
from calibre.gui2.preferences.misc_ui import Ui_Form from calibre.gui2.preferences.misc_ui import Ui_Form
from calibre.gui2 import error_dialog, config, open_local_file, info_dialog from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
from calibre.constants import isosx from calibre.constants import isosx
from calibre import get_proxies
class WorkersSetting(Setting): class WorkersSetting(Setting):
@ -33,6 +34,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.user_defined_device_button.clicked.connect(self.user_defined_device) self.user_defined_device_button.clicked.connect(self.user_defined_device)
self.button_osx_symlinks.clicked.connect(self.create_symlinks) self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx) self.button_osx_symlinks.setVisible(isosx)
proxies = get_proxies(debug=False)
txt = _('No proxies used')
if proxies:
lines = ['<br><code>%s: %s</code>'%(t, p) for t, p in
proxies.iteritems()]
txt = _('<b>Using proxies:</b>') + ''.join(lines)
self.proxies.setText(txt)
def debug_device_detection(self, *args): def debug_device_detection(self, *args):
from calibre.gui2.preferences.device_debug import DebugDevice from calibre.gui2.preferences.device_debug import DebugDevice

View File

@ -118,7 +118,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="20" column="0"> <item row="21" column="0">
<spacer name="verticalSpacer_9"> <spacer name="verticalSpacer_9">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -131,6 +131,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="10" column="0" colspan="2">
<widget class="QLabel" name="proxies">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -627,7 +627,8 @@ class TagTreeItem(object): # {{{
except: except:
pass pass
self.parent = self.icon_state_map = self.bold_font = self.tag = \ self.parent = self.icon_state_map = self.bold_font = self.tag = \
self.icon = self.children = None self.icon = self.children = self.tooltip = \
self.py_name = self.id_set = self.category_key = None
def __str__(self): def __str__(self):
if self.type == self.ROOT: if self.type == self.ROOT:
@ -1121,7 +1122,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.search_restriction = s self.search_restriction = s
def get_node_tree(self, sort): def get_node_tree(self, sort):
old_row_map = self.row_map[:] old_row_map_len = len(self.row_map)
self.row_map = [] self.row_map = []
self.categories = {} self.categories = {}
@ -1176,7 +1177,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map.append(category) self.row_map.append(category)
self.categories[category] = tb_categories[category]['name'] self.categories[category] = tb_categories[category]['name']
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map): if old_row_map_len != 0 and old_row_map_len != len(self.row_map):
# A category has been added or removed. We must force a rebuild of # A category has been added or removed. We must force a rebuild of
# the model # the model
return None return None
@ -1367,6 +1368,9 @@ class TagsModel(QAbstractItemModel): # {{{
self.beginRemoveRows(self.createIndex(category.row(), 0, category), self.beginRemoveRows(self.createIndex(category.row(), 0, category),
start, len(child_map)-1) start, len(child_map)-1)
category.children = ctags category.children = ctags
for i in range(start, len(child_map)):
child_map[i].break_cycles()
child_map = None
self.endRemoveRows() self.endRemoveRows()
else: else:
state_map = {} state_map = {}

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
The database used to store ebook metadata The database used to store ebook metadata
''' '''
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, \ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, \
json, uuid, tempfile json, uuid, tempfile, hashlib
import threading, random import threading, random
from itertools import repeat from itertools import repeat
from math import ceil from math import ceil
@ -1118,9 +1118,31 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return self.format_abspath(index, format, index_is_id) is not None return self.format_abspath(index, format, index_is_id) is not None
def format_last_modified(self, id_, fmt): def format_last_modified(self, id_, fmt):
m = self.format_metadata(id_, fmt)
if m:
return m['mtime']
def format_metadata(self, id_, fmt):
path = self.format_abspath(id_, fmt, index_is_id=True) path = self.format_abspath(id_, fmt, index_is_id=True)
ans = {}
if path is not None: if path is not None:
return utcfromtimestamp(os.stat(path).st_mtime) stat = os.stat(path)
ans['size'] = stat.st_size
ans['mtime'] = utcfromtimestamp(stat.st_mtime)
return ans
def format_hash(self, id_, fmt):
path = self.format_abspath(id_, fmt, index_is_id=True)
if path is None:
raise NoSuchFormat('Record %d has no fmt: %s'%(id_, fmt))
sha = hashlib.sha256()
with lopen(path, 'rb') as f:
while True:
raw = f.read(SPOOL_SIZE)
sha.update(raw)
if len(raw) < SPOOL_SIZE:
break
return sha.hexdigest()
def format_abspath(self, index, format, index_is_id=False): def format_abspath(self, index, format, index_is_id=False):
''' '''

File diff suppressed because it is too large Load Diff

View File

@ -123,6 +123,14 @@ def isoformat(date_time, assume_utc=False, as_utc=True, sep='T'):
date_time = date_time.astimezone(_utc_tz if as_utc else _local_tz) date_time = date_time.astimezone(_utc_tz if as_utc else _local_tz)
return unicode(date_time.isoformat(sep)) return unicode(date_time.isoformat(sep))
def as_local_time(date_time, assume_utc=True):
if not hasattr(date_time, 'tzinfo'):
return date_time
if date_time.tzinfo is None:
date_time = date_time.replace(tzinfo=_utc_tz if assume_utc else
_local_tz)
return date_time.astimezone(_local_tz)
def now(): def now():
return datetime.now().replace(tzinfo=_local_tz) return datetime.now().replace(tzinfo=_local_tz)

View File

@ -8,61 +8,157 @@ __docformat__ = 'restructuredtext en'
''' '''
Measure memory usage of the current process. Measure memory usage of the current process.
The key function is memory() which returns the current memory usage in bytes. The key function is memory() which returns the current memory usage in MB.
You can pass a number to memory and it will be subtracted from the returned You can pass a number to memory and it will be subtracted from the returned
value. value.
''' '''
import gc, os import gc, os, re
from calibre.constants import iswindows, islinux from calibre.constants import iswindows, islinux
if islinux: if islinux:
## {{{ http://code.activestate.com/recipes/286222/ (r1) # Taken, with thanks, from:
# http://wingolog.org/archives/2007/11/27/reducing-the-footprint-of-python-applications
_proc_status = '/proc/%d/status' % os.getpid() def permute(args):
ret = []
if args:
first = args.pop(0)
for y in permute(args):
for x in first:
ret.append(x + y)
else:
ret.append('')
return ret
_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0, def parsed_groups(match, *types):
'KB': 1024.0, 'MB': 1024.0*1024.0} groups = match.groups()
assert len(groups) == len(types)
return tuple([type(group) for group, type in zip(groups, types)])
def _VmB(VmKey): class VMA(dict):
'''Private. def __init__(self, *args):
''' (self.start, self.end, self.perms, self.offset,
global _proc_status, _scale self.major, self.minor, self.inode, self.filename) = args
# get pseudo file /proc/<pid>/status
try:
t = open(_proc_status)
v = t.read()
t.close()
except:
return 0.0 # non-Linux?
# get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
i = v.index(VmKey)
v = v[i:].split(None, 3) # whitespace
if len(v) < 3:
return 0.0 # invalid format?
# convert Vm value to bytes
return float(v[1]) * _scale[v[2]]
def parse_smaps(pid):
with open('/proc/%s/smaps'%pid, 'r') as maps:
hex = lambda s: int(s, 16)
ret = []
header = re.compile(r'^([0-9a-f]+)-([0-9a-f]+) (....) ([0-9a-f]+) '
r'(..):(..) (\d+) *(.*)$')
detail = re.compile(r'^(.*): +(\d+) kB')
for line in maps:
m = header.match(line)
if m:
vma = VMA(*parsed_groups(m, hex, hex, str, hex, str, str, int, str))
ret.append(vma)
else:
m = detail.match(line)
if m:
k, v = parsed_groups(m, str, int)
assert k not in vma
vma[k] = v
else:
print 'unparseable line:', line
return ret
perms = permute(['r-', 'w-', 'x-', 'ps'])
def make_summary_dicts(vmas):
mapped = {}
anon = {}
for d in mapped, anon:
# per-perm
for k in perms:
d[k] = {}
d[k]['Size'] = 0
for y in 'Shared', 'Private':
d[k][y] = {}
for z in 'Clean', 'Dirty':
d[k][y][z] = 0
# totals
for y in 'Shared', 'Private':
d[y] = {}
for z in 'Clean', 'Dirty':
d[y][z] = 0
for vma in vmas:
if vma.major == '00' and vma.minor == '00':
d = anon
else:
d = mapped
for y in 'Shared', 'Private':
for z in 'Clean', 'Dirty':
d[vma.perms][y][z] += vma.get(y + '_' + z, 0)
d[y][z] += vma.get(y + '_' + z, 0)
d[vma.perms]['Size'] += vma.get('Size', 0)
return mapped, anon
def values(d, args):
if args:
ret = ()
first = args[0]
for k in first:
ret += values(d[k], args[1:])
return ret
else:
return (d,)
def print_summary(dicts_and_titles):
def desc(title, perms):
ret = {('Anonymous', 'rw-p'): 'Data (malloc, mmap)',
('Anonymous', 'rwxp'): 'Writable code (stack)',
('Mapped', 'r-xp'): 'Code',
('Mapped', 'rwxp'): 'Writable code (jump tables)',
('Mapped', 'r--p'): 'Read-only data',
('Mapped', 'rw-p'): 'Data'}.get((title, perms), None)
if ret:
return ' -- ' + ret
else:
return ''
for d, title in dicts_and_titles:
print title, 'memory:'
print ' Shared Private'
print ' Clean Dirty Clean Dirty'
for k in perms:
if d[k]['Size']:
print (' %s %7d %7d %7d %7d%s'
% ((k,)
+ values(d[k], (('Shared', 'Private'),
('Clean', 'Dirty')))
+ (desc(title, k),)))
print (' total %7d %7d %7d %7d'
% values(d, (('Shared', 'Private'),
('Clean', 'Dirty'))))
print ' ' + '-' * 40
print (' total %7d %7d %7d %7d'
% tuple(map(sum, zip(*[values(d, (('Shared', 'Private'),
('Clean', 'Dirty')))
for d, title in dicts_and_titles]))))
def print_stats(pid=None):
if pid is None:
pid = os.getpid()
vmas = parse_smaps(pid)
mapped, anon = make_summary_dicts(vmas)
print_summary(((mapped, "Mapped"), (anon, "Anonymous")))
def linux_memory(since=0.0): def linux_memory(since=0.0):
'''Return memory usage in bytes. vmas = parse_smaps(os.getpid())
''' mapped, anon = make_summary_dicts(vmas)
return _VmB('VmSize:') - since dicts_and_titles = ((mapped, "Mapped"), (anon, "Anonymous"))
totals = tuple(map(sum, zip(*[values(d, (('Shared', 'Private'),
('Clean', 'Dirty')))
for d, title in dicts_and_titles])))
return (totals[-1]/1024.) - since
def resident(since=0.0):
'''Return resident memory usage in bytes.
'''
return _VmB('VmRSS:') - since
def stacksize(since=0.0):
'''Return stack size in bytes.
'''
return _VmB('VmStk:') - since
## end of http://code.activestate.com/recipes/286222/ }}}
memory = linux_memory memory = linux_memory
elif iswindows: elif iswindows:
import win32process import win32process
import win32con import win32con
@ -95,7 +191,7 @@ elif iswindows:
def win_memory(since=0.0): def win_memory(since=0.0):
info = meminfo(get_handle(os.getpid())) info = meminfo(get_handle(os.getpid()))
return info['WorkingSetSize'] - since return (info['WorkingSetSize']/1024.**2) - since
memory = win_memory memory = win_memory