diff --git a/resources/recipes/blic.recipe b/resources/recipes/blic.recipe index 0c955bebde..384518ec13 100644 --- a/resources/recipes/blic.recipe +++ b/resources/recipes/blic.recipe @@ -1,6 +1,6 @@ __license__ = 'GPL v3' -__copyright__ = '2008-2010, Darko Miletic ' +__copyright__ = '2008-2011, Darko Miletic ' ''' blic.rs ''' @@ -21,21 +21,53 @@ class Blic(BasicNewsRecipe): masthead_url = 'http://www.blic.rs/resources/images/header/header_back.png' language = 'sr' publication_type = 'newspaper' - extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Georgia, serif1, serif} .article_description{font-family: Arial, sans1, sans-serif} .img_full{float: none} img{margin-bottom: 0.8em} ' + extra_css = """ + @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} + @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} + body{font-family: Georgia, serif1, serif} + .articledescription,#nadnaslov,.article_info{font-family: Arial, sans1, sans-serif} + .img_full{float: none} + #nadnaslov{font-size: small} + #article_lead{font-size: 1.5em} + h1{color: red} + .potpis{font-size: x-small; color: gray} + .article_info{font-size: small} + img{margin-bottom: 0.8em; margin-top: 0.8em; display: block} + """ conversion_options = { 'comment' : description , 'tags' : category , 'publisher': publisher , 'language' : language + , 'linearize_tables' : True } preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] remove_tags_before = dict(name='div', attrs={'id':'article_info'}) - remove_tags = [dict(name=['object','link'])] - remove_attributes = ['width','height'] + remove_tags = [dict(name=['object','link','meta','base','object','embed'])] + remove_attributes = ['width','height','m_id','m_ext','mlg_id','poll_id','v_id'] - feeds = [(u'Danasnje Vesti', u'http://www.blic.rs/rss/danasnje-vesti')] + feeds = [ + (u'Politika' , u'http://www.blic.rs/rss/Vesti/Politika') + ,(u'Tema Dana' , u'http://www.blic.rs/rss/Vesti/Tema-Dana') + ,(u'Svet' , u'http://www.blic.rs/rss/Vesti/Svet') + ,(u'Drustvo' , u'http://www.blic.rs/rss/Vesti/Drustvo') + ,(u'Ekonomija' , u'http://www.blic.rs/rss/Vesti/Ekonomija') + ,(u'Hronika' , u'http://www.blic.rs/rss/Vesti/Hronika') + ,(u'Beograd' , u'http://www.blic.rs/rss/Vesti/Beograd') + ,(u'Srbija' , u'http://www.blic.rs/rss/Vesti/Srbija') + ,(u'Vojvodina' , u'http://www.blic.rs/rss/Vesti/Vojvodina') + ,(u'Republika Srpska' , u'http://www.blic.rs/rss/Vesti/Republika-Srpska') + ,(u'Reportaza' , u'http://www.blic.rs/rss/Vesti/Reportaza') + ,(u'Dodatak' , u'http://www.blic.rs/rss/Vesti/Dodatak') + ,(u'Zabava' , u'http://www.blic.rs/rss/Zabava') + ,(u'Kultura' , u'http://www.blic.rs/rss/Kultura') + ,(u'Slobodno Vreme' , u'http://www.blic.rs/rss/Slobodno-vreme') + ,(u'IT' , u'http://www.blic.rs/rss/IT') + ,(u'Komentar' , u'http://www.blic.rs/rss/Komentar') + ,(u'Intervju' , u'http://www.blic.rs/rss/Intervju') + ] def print_version(self, url): @@ -44,4 +76,4 @@ class Blic(BasicNewsRecipe): def preprocess_html(self, soup): for item in soup.findAll(style=True): del item['style'] - return self.adeify_images(soup) + return soup diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index a4f7439405..221f5911c6 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -241,7 +241,7 @@ def get_parsed_proxy(typ='http', debug=True): return ans -def browser(honor_time=True, max_time=2, mobile_browser=False): +def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None): ''' Create a mechanize browser for web scraping. The browser handles cookies, refresh requests and ignores robots.txt. Also uses proxy if avaialable. @@ -253,8 +253,10 @@ def browser(honor_time=True, max_time=2, mobile_browser=False): opener = Browser() opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time) opener.set_handle_robots(False) - opener.addheaders = [('User-agent', ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \ - 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13')] + if user_agent is None: + user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \ + 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13' + opener.addheaders = [('User-agent', user_agent)] http_proxy = get_proxies().get('http', None) if http_proxy: opener.set_proxies({'http':http_proxy}) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 277070020b..a95e3c46fa 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -21,7 +21,7 @@ class ANDROID(USBMS): # HTC 0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100, 0x0227], 0x0ff9 : [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226], - 0xc92 : [0x100], 0xc97: [0x226]}, + 0xc92 : [0x100], 0xc97: [0x226], 0xc99 : [0x0100]}, # Eken 0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] }, @@ -54,7 +54,7 @@ class ANDROID(USBMS): 0x1004 : { 0x61cc : [0x100] }, # Archos - 0x0e79 : { 0x1420 : [0x0216]}, + 0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216]}, } EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books'] @@ -70,10 +70,10 @@ class ANDROID(USBMS): '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', - 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID'] + 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', - 'A70S'] + 'A70S', 'A101IT'] OSX_MAIN_MEM = 'Android Device Main Memory' diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index 5fceeb7aed..691aa307d7 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -224,6 +224,10 @@ class Dehyphenator(object): return firsthalf+u'\u2014'+wraptags+secondhalf else: + if self.format == 'individual_words' and len(firsthalf) + len(secondhalf) <= 6: + if self.verbose > 2: + self.log("too short, returned hyphenated word: " + str(hyphenated)) + return hyphenated if len(firsthalf) <= 2 and len(secondhalf) <= 2: if self.verbose > 2: self.log("too short, returned hyphenated word: " + str(hyphenated)) diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py index 7f312da1d9..d956747a2b 100644 --- a/src/calibre/ebooks/metadata/library_thing.py +++ b/src/calibre/ebooks/metadata/library_thing.py @@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' Fetch cover from LibraryThing.com based on ISBN number. ''' -import sys, socket, os, re +import sys, socket, os, re, random from lxml import html import mechanize @@ -16,13 +16,26 @@ from calibre.ebooks.chardet import strip_encoding_declarations OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' +def get_ua(): + choices = [ + 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11' + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)' + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)' + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)' + 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16' + 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19' + 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11' + ] + return choices[random.randint(0, len(choices)-1)] + + class HeadRequest(mechanize.Request): def get_method(self): return 'HEAD' def check_for_cover(isbn, timeout=5.): - br = browser() + br = browser(user_agent=get_ua()) br.set_handle_redirect(False) try: br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout) @@ -51,7 +64,7 @@ def login(br, username, password, force=True): def cover_from_isbn(isbn, timeout=5., username=None, password=None): src = None - br = browser() + br = browser(user_agent=get_ua()) try: return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg' except: @@ -100,7 +113,7 @@ def get_social_metadata(title, authors, publisher, isbn, username=None, from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(title, authors) if isbn: - br = browser() + br = browser(user_agent=get_ua()) if username and password: try: login(br, username, password, force=False) diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py index 5aa4979b04..1fb14eb06f 100644 --- a/src/calibre/ebooks/rtf/rtfml.py +++ b/src/calibre/ebooks/rtf/rtfml.py @@ -262,7 +262,7 @@ class RTFMLizer(object): if hasattr(elem, 'tail') and elem.tail != None and elem.tail.strip() != '': if 'block' in tag_stack: - text += '%s ' % txt2rtf(elem.tail) + text += '%s' % txt2rtf(elem.tail) else: text += '{\\par \\pard \\hyphpar %s}' % txt2rtf(elem.tail) diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index 9fd8af0d70..43aadc6576 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -175,9 +175,9 @@ def detect_formatting_type(txt): # Block quote. textile_count += len(re.findall(r'(?mu)^bq\.', txt)) # Images - textile_count += len(re.findall(r'\![^\s]+(:[^\s]+)*', txt)) + textile_count += len(re.findall(r'\![^\s]+(?=.*?/)(:[^\s]+)*', txt)) # Links - textile_count += len(re.findall(r'"(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt)) + textile_count += len(re.findall(r'"(?=".*?\()(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt)) if markdown_count > 5 or textile_count > 5: if markdown_count > textile_count: diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 6fa53d6290..7c454d0a94 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -8,11 +8,12 @@ __docformat__ = 'restructuredtext en' import os from functools import partial -from PyQt4.Qt import QInputDialog, QPixmap, QMenu +from PyQt4.Qt import QPixmap, QMenu from calibre.gui2 import error_dialog, choose_files, \ choose_dir, warning_dialog, info_dialog +from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog from calibre.gui2.widgets import IMAGE_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS from calibre.utils.filenames import ascii_filename @@ -42,7 +43,7 @@ class AddAction(InterfaceAction): 'ebook file is a different book)'), self.add_recursive_multiple) self.add_menu.addSeparator() self.add_menu.addAction(_('Add Empty book. (Book entry with no ' - 'formats)'), self.add_empty) + 'formats)'), self.add_empty, _('Shift+Ctrl+E')) self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn) self.qaction.setMenu(self.add_menu) self.qaction.triggered.connect(self.add_books) @@ -83,12 +84,21 @@ class AddAction(InterfaceAction): Add an empty book item to the library. This does not import any formats from a book file. ''' - num, ok = QInputDialog.getInt(self.gui, _('How many empty books?'), - _('How many empty books should be added?'), 1, 1, 100) - if ok: + author = None + index = self.gui.library_view.currentIndex() + if index.isValid(): + raw = index.model().db.authors(index.row()) + if raw: + authors = [a.strip().replace('|', ',') for a in raw.split(',')] + if authors: + author = authors[0] + dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author) + if dlg.exec_() == dlg.Accepted: + num = dlg.qty_to_add from calibre.ebooks.metadata import MetaInformation for x in xrange(num): - self.gui.library_view.model().db.import_book(MetaInformation(None), []) + mi = MetaInformation(_('Unknown'), dlg.selected_authors) + self.gui.library_view.model().db.import_book(mi, []) self.gui.library_view.model().books_added(num) def add_isbns(self, books, add_tags=[]): diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 6f4e883b1a..930e5e29aa 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -32,7 +32,7 @@ class LibraryUsageStats(object): # {{{ locs = list(self.stats.keys()) locs.sort(cmp=lambda x, y: cmp(self.stats[x], self.stats[y]), reverse=True) - for key in locs[15:]: + for key in locs[25:]: self.stats.pop(key) gprefs.set('library_usage_stats', self.stats) @@ -384,7 +384,14 @@ class ChooseLibraryAction(InterfaceAction): return prefs['library_path'] = loc + #from calibre.utils.mem import memory + #import weakref, gc + #ref = weakref.ref(self.gui.library_view.model().db) + #before = memory()/1024**2 self.gui.library_moved(loc) + #print gc.get_referrers(ref)[0] + #for i in xrange(3): gc.collect() + #print 'leaked:', memory()/1024**2 - before def qs_requested(self, idx, *args): self.switch_requested(self.qs_locations[idx]) diff --git a/src/calibre/gui2/convert/regex_builder.py b/src/calibre/gui2/convert/regex_builder.py index c5c8d84a88..ca6e429176 100644 --- a/src/calibre/gui2/convert/regex_builder.py +++ b/src/calibre/gui2/convert/regex_builder.py @@ -47,6 +47,8 @@ class RegexBuilder(QDialog, Ui_RegexBuilder): return False else: self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }') + self.preview.setExtraSelections([]) + return False return True def do_test(self): diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 58985d1121..c873d1ed94 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -379,7 +379,8 @@ def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, pa w = bulk_widgets[type](db, col, parent) else: w = widgets[type](db, col, parent) - w.initialize(book_id) + if book_id is not None: + w.initialize(book_id) return w x = db.custom_column_num_map cols = list(x) diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py new file mode 100644 index 0000000000..b8339f95f5 --- /dev/null +++ b/src/calibre/gui2/dialogs/add_empty_book.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + + +from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \ + QApplication, QSpinBox, QToolButton, QIcon +from calibre.ebooks.metadata import authors_to_string, string_to_authors +from calibre.gui2.widgets import CompleteComboBox +from calibre.utils.icu import sort_key + +class AddEmptyBookDialog(QDialog): + + def __init__(self, parent, db, author): + QDialog.__init__(self, parent) + self.db = db + + self.setWindowTitle(_('How many empty books?')) + + self._layout = QGridLayout(self) + self.setLayout(self._layout) + + self.qty_label = QLabel(_('How many empty books should be added?')) + self._layout.addWidget(self.qty_label, 0, 0, 1, 2) + + self.qty_spinbox = QSpinBox(self) + self.qty_spinbox.setRange(1, 10000) + self.qty_spinbox.setValue(1) + self._layout.addWidget(self.qty_spinbox, 1, 0, 1, 2) + + self.author_label = QLabel(_('Set the author of the new books to:')) + self._layout.addWidget(self.author_label, 2, 0, 1, 2) + + self.authors_combo = CompleteComboBox(self) + self.authors_combo.setSizeAdjustPolicy( + self.authors_combo.AdjustToMinimumContentsLengthWithIcon) + self.authors_combo.setEditable(True) + self._layout.addWidget(self.authors_combo, 3, 0, 1, 1) + self.initialize_authors(db, author) + + self.clear_button = QToolButton(self) + self.clear_button.setIcon(QIcon(I('trash.png'))) + self.clear_button.setToolTip(_('Reset author to Unknown')) + self.clear_button.clicked.connect(self.reset_author) + self._layout.addWidget(self.clear_button, 3, 1, 1, 1) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + self._layout.addWidget(button_box) + self.resize(self.sizeHint()) + + def reset_author(self, *args): + self.authors_combo.setEditText(_('Unknown')) + + def initialize_authors(self, db, author): + all_authors = db.all_authors() + all_authors.sort(key=lambda x : sort_key(x[1])) + for i in all_authors: + id, name = i + name = [name.strip().replace('|', ',') for n in name.split(',')] + self.authors_combo.addItem(authors_to_string(name)) + + au = author + if not au: + au = _('Unknown') + self.authors_combo.setEditText(au.replace('|', ',')) + + self.authors_combo.set_separator('&') + self.authors_combo.set_space_before_sep(True) + self.authors_combo.update_items_cache(db.all_author_names()) + + @property + def qty_to_add(self): + return self.qty_spinbox.value() + + @property + def selected_authors(self): + return string_to_authors(unicode(self.authors_combo.text())) + +if __name__ == '__main__': + app = QApplication([]) + d = AddEmptyBookDialog() + d.exec_() diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index a2ced18e0f..00bc98cb17 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -775,7 +775,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.original_tags = unicode(self.tags.text()) else: self.tags.setText(self.original_tags) - d = TagEditor(self, self.db, self.row) + d = TagEditor(self, self.db, self.id) d.exec_() if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) diff --git a/src/calibre/gui2/dialogs/tag_editor.py b/src/calibre/gui2/dialogs/tag_editor.py index 6c5aa6de66..c12b7357f1 100644 --- a/src/calibre/gui2/dialogs/tag_editor.py +++ b/src/calibre/gui2/dialogs/tag_editor.py @@ -10,13 +10,13 @@ from calibre.utils.icu import sort_key class TagEditor(QDialog, Ui_TagEditor): - def __init__(self, window, db, index=None): + def __init__(self, window, db, id_=None): QDialog.__init__(self, window) Ui_TagEditor.__init__(self) self.setupUi(self) self.db = db - self.index = index + self.index = db.row(id_) if self.index is not None: tags = self.db.tags(self.index) else: diff --git a/src/calibre/gui2/dialogs/user_profiles.py b/src/calibre/gui2/dialogs/user_profiles.py index 04c41f0c5e..fe64deb430 100644 --- a/src/calibre/gui2/dialogs/user_profiles.py +++ b/src/calibre/gui2/dialogs/user_profiles.py @@ -356,6 +356,13 @@ class %(classname)s(%(base_class)s): self.populate_options(AutomaticNewsRecipe) self.source_code.setText('') + def reject(self): + if question_dialog(self, _('Are you sure?'), + _('You will lose any unsaved changes. To save your' + ' changes, click the Add/Update recipe button.' + ' Continue?'), show_copy_button=False): + ResizableDialog.reject(self) + if __name__ == '__main__': from calibre.gui2 import is_ok_to_use_qt is_ok_to_use_qt() diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py new file mode 100644 index 0000000000..96ea785ff2 --- /dev/null +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -0,0 +1,1014 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import textwrap, re, os + +from PyQt4.Qt import Qt, QDateEdit, QDate, \ + QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ + QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ + QPushButton, QSpinBox, QMessageBox, QLineEdit + +from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ + EnComboBox, FormatList, ImageView, CompleteLineEdit +from calibre.utils.icu import sort_key +from calibre.utils.config import tweaks, prefs +from calibre.ebooks.metadata import title_sort, authors_to_string, \ + string_to_authors, check_isbn +from calibre.ebooks.metadata.meta import get_metadata +from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ + choose_files, error_dialog, choose_images, question_dialog +from calibre.utils.date import local_tz, qt_to_dt +from calibre import strftime +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.customize.ui import run_plugins_on_import +from calibre.utils.date import utcfromtimestamp +from calibre.gui2.comments_editor import Editor +from calibre.library.comments import comments_to_html +from calibre.gui2.dialogs.tag_editor import TagEditor + +''' +The interface common to all widgets used to set basic metadata +class BasicMetadataWidget(object): + + LABEL = "label text" + + def initialize(self, db, id_): + pass + + def commit(self, db, id_): + return True + + @dynamic_property + def current_val(self): + # Present in most but not all basic metadata widgets + def fget(self): + return None + def fset(self, val): + pass + return property(fget=fget, fset=fset) +''' + +# Title {{{ +class TitleEdit(EnLineEdit): + + TITLE_ATTR = 'title' + COMMIT = True + TOOLTIP = _('Change the title of this book') + LABEL = _('&Title:') + + def __init__(self, parent): + self.dialog = parent + EnLineEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + + def get_default(self): + return _('Unknown') + + def initialize(self, db, id_): + title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True) + self.current_val = title + self.original_val = self.current_val + + def commit(self, db, id_): + title = self.current_val + if self.COMMIT: + getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False) + else: + getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False, + commit=False) + return True + + @dynamic_property + def current_val(self): + + def fget(self): + title = unicode(self.text()).strip() + if not title: + title = self.get_default() + return title + + def fset(self, val): + if hasattr(val, 'strip'): + val = val.strip() + if not val: + val = self.get_default() + self.setText(val) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + +class TitleSortEdit(TitleEdit): + + TITLE_ATTR = 'title_sort' + COMMIT = False + TOOLTIP = _('Specify how this book should be sorted when by title.' + ' For example, The Exorcist might be sorted as Exorcist, The.') + LABEL = _('Title &sort:') + + def __init__(self, parent, title_edit, autogen_button): + TitleEdit.__init__(self, parent) + self.title_edit = title_edit + + base = self.TOOLTIP + ok_tooltip = '

' + textwrap.fill(base+'

'+ + _(' The green color indicates that the current ' + 'title sort matches the current title')) + bad_tooltip = '

'+textwrap.fill(base + '

'+ + _(' The red color warns that the current ' + 'title sort does not match the current title. ' + 'No action is required if this is what you want.')) + self.tooltips = (ok_tooltip, bad_tooltip) + + self.title_edit.textChanged.connect(self.update_state) + self.textChanged.connect(self.update_state) + + autogen_button.clicked.connect(self.auto_generate) + self.update_state() + + def update_state(self, *args): + ts = title_sort(self.title_edit.current_val) + normal = ts == self.current_val + if normal: + col = 'rgb(0, 255, 0, 20%)' + else: + col = 'rgb(255, 0, 0, 20%)' + self.setStyleSheet('QLineEdit { color: black; ' + 'background-color: %s; }'%col) + tt = self.tooltips[0 if normal else 1] + self.setToolTip(tt) + self.setWhatsThis(tt) + + def auto_generate(self, *args): + self.current_val = title_sort(self.title_edit.current_val) + +# }}} + +# Authors {{{ +class AuthorsEdit(CompleteComboBox): + + TOOLTIP = '' + LABEL = _('&Author(s):') + + def __init__(self, parent): + self.dialog = parent + CompleteComboBox.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setEditable(True) + self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) + + def get_default(self): + return _('Unknown') + + def initialize(self, db, id_): + all_authors = db.all_authors() + all_authors.sort(key=lambda x : sort_key(x[1])) + for i in all_authors: + id, name = i + name = [name.strip().replace('|', ',') for n in name.split(',')] + self.addItem(authors_to_string(name)) + + self.set_separator('&') + self.set_space_before_sep(True) + self.update_items_cache(db.all_author_names()) + + au = db.authors(id_, index_is_id=True) + if not au: + au = _('Unknown') + self.current_val = [a.strip().replace('|', ',') for a in au.split(',')] + self.original_val = self.current_val + + def commit(self, db, id_): + authors = self.current_val + db.set_authors(id_, authors, notify=False) + return True + + @dynamic_property + def current_val(self): + + def fget(self): + au = unicode(self.text()).strip() + if not au: + au = self.get_default() + return string_to_authors(au) + + def fset(self, val): + if not val: + val = [self.get_default()] + self.setEditText(' & '.join([x.strip() for x in val])) + self.lineEdit().setCursorPosition(0) + + + return property(fget=fget, fset=fset) + +class AuthorSortEdit(EnLineEdit): + + TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' + 'For example Charles Dickens should be sorted as Dickens, ' + 'Charles.\nIf the box is colored green, then text matches ' + 'the individual author\'s sort strings. If it is colored ' + 'red, then the authors and this text do not match.') + LABEL = _('Author s&ort:') + + def __init__(self, parent, authors_edit, autogen_button, db): + EnLineEdit.__init__(self, parent) + self.authors_edit = authors_edit + self.db = db + + base = self.TOOLTIP + ok_tooltip = '

' + textwrap.fill(base+'

'+ + _(' The green color indicates that the current ' + 'author sort matches the current author')) + bad_tooltip = '

'+textwrap.fill(base + '

'+ + _(' The red color indicates that the current ' + 'author sort does not match the current author. ' + 'No action is required if this is what you want.')) + self.tooltips = (ok_tooltip, bad_tooltip) + + self.authors_edit.editTextChanged.connect(self.update_state) + self.textChanged.connect(self.update_state) + + autogen_button.clicked.connect(self.auto_generate) + self.update_state() + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.text()).strip() + + def fset(self, val): + if not val: + val = '' + self.setText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def update_state(self, *args): + au = unicode(self.authors_edit.text()) + au = re.sub(r'\s+et al\.$', '', au) + au = self.db.author_sort_from_authors(string_to_authors(au)) + + normal = au == self.current_val + if normal: + col = 'rgb(0, 255, 0, 20%)' + else: + col = 'rgb(255, 0, 0, 20%)' + self.setStyleSheet('QLineEdit { color: black; ' + 'background-color: %s; }'%col) + tt = self.tooltips[0 if normal else 1] + self.setToolTip(tt) + self.setWhatsThis(tt) + + def auto_generate(self, *args): + au = unicode(self.authors_edit.text()) + au = re.sub(r'\s+et al\.$', '', au) + authors = string_to_authors(au) + self.current_val = self.db.author_sort_from_authors(authors) + + def initialize(self, db, id_): + self.current_val = db.author_sort(id_, index_is_id=True) + + def commit(self, db, id_): + aus = self.current_val + db.set_author_sort(id_, aus, notify=False, commit=False) + return True + +# }}} + +# Series {{{ +class SeriesEdit(EnComboBox): + + TOOLTIP = _('List of known series. You can add new series.') + LABEL = _('&Series:') + + def __init__(self, parent): + EnComboBox.__init__(self, parent) + self.dialog = parent + self.setSizeAdjustPolicy( + self.AdjustToMinimumContentsLengthWithIcon) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setEditable(True) + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.currentText()).strip() + + def fset(self, val): + if not val: + val = '' + self.setEditText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + all_series = db.all_series() + all_series.sort(key=lambda x : sort_key(x[1])) + series_id = db.series_id(id_, index_is_id=True) + idx, c = None, 0 + for i in all_series: + id, name = i + if id == series_id: + idx = c + self.addItem(name) + c += 1 + + self.lineEdit().setText('') + if idx is not None: + self.setCurrentIndex(idx) + self.original_val = self.current_val + + def commit(self, db, id_): + series = self.current_val + db.set_series(id_, series, notify=False, commit=True) + return True + +class SeriesIndexEdit(QDoubleSpinBox): + + TOOLTIP = '' + LABEL = _('&Number:') + + def __init__(self, parent, series_edit): + QDoubleSpinBox.__init__(self, parent) + self.dialog = parent + self.db = self.original_series_name = None + self.setMaximum(1000000) + self.series_edit = series_edit + series_edit.currentIndexChanged.connect(self.enable) + series_edit.editTextChanged.connect(self.enable) + series_edit.lineEdit().editingFinished.connect(self.increment) + self.enable() + + def enable(self, *args): + self.setEnabled(bool(self.series_edit.current_val)) + + @dynamic_property + def current_val(self): + + def fget(self): + return self.value() + + def fset(self, val): + if val is None: + val = 1.0 + val = float(val) + self.setValue(val) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.db = db + if self.series_edit.current_val: + val = db.series_index(id_, index_is_id=True) + else: + val = 1.0 + self.current_val = val + self.original_val = self.current_val + self.original_series_name = self.series_edit.original_val + + def commit(self, db, id_): + db.set_series_index(id_, self.current_val, notify=False, commit=False) + return True + + def increment(self): + if self.db is not None: + try: + series = self.series_edit.current_val + if series and series != self.original_series_name: + ns = 1.0 + if tweaks['series_index_auto_increment'] != 'const': + ns = self.db.get_next_series_num_for(series) + self.current_val = ns + self.original_series_name = series + except: + import traceback + traceback.print_exc() + + +# }}} + +class BuddyLabel(QLabel): # {{{ + + def __init__(self, buddy): + QLabel.__init__(self, buddy.LABEL) + self.setBuddy(buddy) + self.setAlignment(Qt.AlignRight|Qt.AlignVCenter) +# }}} + +class Format(QListWidgetItem): # {{{ + + def __init__(self, parent, ext, size, path=None, timestamp=None): + self.path = path + self.ext = ext + self.size = float(size)/(1024*1024) + text = '%s (%.2f MB)'%(self.ext.upper(), self.size) + QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), + text, parent, QListWidgetItem.UserType) + if timestamp is not None: + ts = timestamp.astimezone(local_tz) + t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple()) + text = _('Last modified: %s')%t + self.setToolTip(text) + self.setStatusTip(text) + +# }}} + +class FormatsManager(QWidget): # {{{ + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.dialog = parent + self.changed = False + + self.l = l = QGridLayout() + self.setLayout(l) + self.cover_from_format_button = QToolButton(self) + self.cover_from_format_button.setToolTip( + _('Set the cover for the book from the selected format')) + self.cover_from_format_button.setIcon(QIcon(I('book.png'))) + self.cover_from_format_button.setIconSize(QSize(32, 32)) + + self.metadata_from_format_button = QToolButton(self) + self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) + self.metadata_from_format_button.setIconSize(QSize(32, 32)) + + self.add_format_button = QToolButton(self) + self.add_format_button.setIcon(QIcon(I('add_book.png'))) + self.add_format_button.setIconSize(QSize(32, 32)) + self.add_format_button.clicked.connect(self.add_format) + + self.remove_format_button = QToolButton(self) + self.remove_format_button.setIcon(QIcon(I('trash.png'))) + self.remove_format_button.setIconSize(QSize(32, 32)) + self.remove_format_button.clicked.connect(self.remove_format) + + self.formats = FormatList(self) + self.formats.setAcceptDrops(True) + self.formats.formats_dropped.connect(self.formats_dropped) + self.formats.delete_format.connect(self.remove_format) + self.formats.itemDoubleClicked.connect(self.show_format) + self.formats.setDragDropMode(self.formats.DropOnly) + self.formats.setIconSize(QSize(32, 32)) + self.formats.setMaximumWidth(200) + + l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) + l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1) + l.addWidget(self.add_format_button, 0, 2, 1, 1) + l.addWidget(self.remove_format_button, 2, 2, 1, 1) + l.addWidget(self.formats, 0, 1, 3, 1) + + + + def initialize(self, db, id_): + self.changed = False + exts = db.formats(id_, index_is_id=True) + if exts: + exts = exts.split(',') + for ext in exts: + if not ext: + ext = '' + size = db.sizeof_format(id_, ext, index_is_id=True) + timestamp = db.format_last_modified(id_, ext) + if size is None: + continue + Format(self.formats, ext, size, timestamp=timestamp) + + def commit(self, db, id_): + if not self.changed: + return True + old_extensions, new_extensions, paths = set(), set(), {} + for row in range(self.formats.count()): + fmt = self.formats.item(row) + ext, path = fmt.ext.lower(), fmt.path + if 'unknown' in ext.lower(): + ext = None + if path: + new_extensions.add(ext) + paths[ext] = path + else: + old_extensions.add(ext) + for ext in new_extensions: + db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False, + index_is_id=True) + db_extensions = set([f.lower() for f in db.formats(id_, + index_is_id=True).split(',')]) + extensions = new_extensions.union(old_extensions) + for ext in db_extensions: + if ext not in extensions: + db.remove_format(id_, ext, notify=False, index_is_id=True) + + self.changed = False + return True + + def add_format(self, *args): + files = choose_files(self, 'add formats dialog', + _("Choose formats for ") + + self.dialog.title.current_val, + [(_('Books'), BOOK_EXTENSIONS)]) + self._add_formats(files) + + def _add_formats(self, paths): + added = False + if not paths: + return added + bad_perms = [] + for _file in paths: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + bad_perms.append(_file) + continue + + nfile = run_plugins_on_import(_file) + if nfile is not None: + _file = nfile + stat = os.stat(_file) + size = stat.st_size + ext = os.path.splitext(_file)[1].lower().replace('.', '') + timestamp = utcfromtimestamp(stat.st_mtime) + for row in range(self.formats.count()): + fmt = self.formats.item(row) + if fmt.ext.lower() == ext: + self.formats.takeItem(row) + break + Format(self.formats, ext, size, path=_file, timestamp=timestamp) + self.changed = True + added = True + if bad_perms: + error_dialog(self, _('No permission'), + _('You do not have ' + 'permission to read the following files:'), + det_msg='\n'.join(bad_perms), show=True) + + return added + + def formats_dropped(self, event, paths): + if self._add_formats(paths): + event.accept() + + def remove_format(self, *args): + rows = self.formats.selectionModel().selectedRows(0) + for row in rows: + self.formats.takeItem(row.row()) + self.changed = True + + def show_format(self, item, *args): + fmt = item.ext + self.dialog.view_format.emit(fmt) + + def get_selected_format_metadata(self, db, id_): + old = prefs['read_file_metadata'] + if not old: + prefs['read_file_metadata'] = True + try: + row = self.formats.currentRow() + fmt = self.formats.item(row) + if fmt is None: + if self.formats.count() == 1: + fmt = self.formats.item(0) + if fmt is None: + error_dialog(self, _('No format selected'), + _('No format selected')).exec_() + return None, None + ext = fmt.ext.lower() + if fmt.path is None: + stream = db.format(id_, ext, as_file=True, index_is_id=True) + else: + stream = open(fmt.path, 'r+b') + try: + mi = get_metadata(stream, ext) + return mi, ext + except: + error_dialog(self, _('Could not read metadata'), + _('Could not read metadata from %s format')%ext).exec_() + return None, None + finally: + if old != prefs['read_file_metadata']: + prefs['read_file_metadata'] = old + +# }}} + +class Cover(ImageView): # {{{ + + def __init__(self, parent): + ImageView.__init__(self, parent) + self.dialog = parent + self._cdata = None + self.cover_changed.connect(self.set_pixmap_from_data) + + self.select_cover_button = QPushButton(QIcon(I('document_open.png')), + _('&Browse'), parent) + self.trim_cover_button = QPushButton(QIcon(I('trim.png')), + _('T&rim'), parent) + self.remove_cover_button = QPushButton(QIcon(I('trash.png')), + _('&Remove'), parent) + + self.select_cover_button.clicked.connect(self.select_cover) + self.remove_cover_button.clicked.connect(self.remove_cover) + self.trim_cover_button.clicked.connect(self.trim_cover) + + self.download_cover_button = QPushButton(_('Download co&ver'), parent) + self.generate_cover_button = QPushButton(_('&Generate cover'), parent) + + self.download_cover_button.clicked.connect(self.download_cover) + self.generate_cover_button.clicked.connect(self.generate_cover) + + self.buttons = [self.select_cover_button, self.remove_cover_button, + self.trim_cover_button, self.download_cover_button, + self.generate_cover_button] + + def select_cover(self, *args): + files = choose_images(self, 'change cover dialog', + _('Choose cover for ') + + self.dialog.title.current_val) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self, _('Cannot read'), + _('You do not have permission to read the file: ') + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self, _('Error reading file'), + _("

There was an error reading from file:
") + + _file + "


"+str(e)) + d.exec_() + if cover: + orig = self.current_val + self.current_val = cover + if self.current_val is None: + self.current_val = orig + error_dialog(self, + _("Not a valid picture"), + _file + _(" is not a valid picture"), show=True) + + def remove_cover(self, *args): + self.current_val = None + + def trim_cover(self, *args): + from calibre.utils.magick import Image + cdata = self.current_val + if not cdata: + return + im = Image() + im.load(cdata) + im.trim(10) + cdata = im.export('png') + self.current_val = cdata + + def download_cover(self, *args): + pass # TODO: Implement this + + def generate_cover(self, *args): + from calibre.ebooks import calibre_cover + from calibre.ebooks.metadata import fmt_sidx + from calibre.gui2 import config + title = self.dialog.title.current_val + author = authors_to_string(self.dialog.authors.current_val) + if not title or not author: + return error_dialog(self, _('Specify title and author'), + _('You must specify a title and author before generating ' + 'a cover'), show=True) + series = self.dialog.series.current_val + series_string = None + if series: + series_string = _('Book %s of %s')%( + fmt_sidx(self.dialog.series_index.current_val, + use_roman=config['use_roman_numerals_for_series_number']), series) + self.current_val = calibre_cover(title, author, + series_string=series_string) + + def set_pixmap_from_data(self, data): + if not data: + self.current_val = None + return + orig = self.current_val + self.current_val = data + if self.current_val is None: + error_dialog(self, _('Invalid cover'), + _('Could not change cover as the image is invalid.'), + show=True) + self.current_val = orig + + def initialize(self, db, id_): + self._cdata = None + self.current_val = db.cover(id_, index_is_id=True) + self.original_val = self.current_val + + @property + def changed(self): + return self.current_val != self.original_val + + @dynamic_property + def current_val(self): + def fget(self): + return self._cdata + def fset(self, cdata): + self._cdata = None + pm = QPixmap() + if cdata: + pm.loadFromData(cdata) + if pm.isNull(): + pm = QPixmap(I('default_cover.png')) + else: + self._cdata = cdata + self.setPixmap(pm) + tt = _('This book has no cover') + if self._cdata: + tt = _('Cover size: %dx%d pixels') % \ + (pm.width(), pm.height()) + self.setToolTip(tt) + + return property(fget=fget, fset=fset) + + def commit(self, db, id_): + if self.changed: + if self.current_val: + db.set_cover(id_, self.current_val, notify=False, commit=False) + else: + db.remove_cover(id_, notify=False, commit=False) + return True + +# }}} + +class CommentsEdit(Editor): # {{{ + + @dynamic_property + def current_val(self): + def fget(self): + return self.html + def fset(self, val): + if not val or not val.strip(): + val = '' + else: + val = comments_to_html(val) + self.html = val + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = db.comments(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_comment(id_, self.current_val, notify=False, commit=False) + return True +# }}} + +class RatingEdit(QSpinBox): # {{{ + LABEL = _('&Rating:') + TOOLTIP = _('Rating of this book. 0-5 stars') + + def __init__(self, parent): + QSpinBox.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setMaximum(5) + self.setSuffix(' ' + _('stars')) + + @dynamic_property + def current_val(self): + def fget(self): + return self.value() + def fset(self, val): + if val is None: + val = 0 + val = int(val) + if val < 0: + val = 0 + if val > 5: + val = 5 + self.setValue(val) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + val = db.rating(id_, index_is_id=True) + if val > 0: + val = int(val/2.) + else: + val = 0 + self.current_val = val + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_rating(id_, 2*self.current_val, notify=False, commit=False) + return True + +# }}} + +class TagsEdit(CompleteLineEdit): # {{{ + LABEL = _('Ta&gs:') + TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' + 'useful while searching.

They can be any words' + 'or phrases, separated by commas.') + + def __init__(self, parent): + CompleteLineEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + + @dynamic_property + def current_val(self): + def fget(self): + return [x.strip() for x in unicode(self.text()).split(',')] + def fset(self, val): + if not val: + val = [] + self.setText(', '.join([x.strip() for x in val])) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + tags = db.tags(id_, index_is_id=True) + tags = tags.split(',') if tags else [] + self.current_val = tags + self.update_items_cache(db.all_tags()) + self.original_val = self.current_val + + @property + def changed(self): + return self.current_val != self.original_val + + def edit(self, db, id_): + if self.changed: + if question_dialog(self, _('Tags changed'), + _('You have changed the tags. In order to use the tags' + ' editor, you must either discard or apply these ' + 'changes'), show_copy_button=False, + buttons=QMessageBox.Apply|QMessageBox.Discard, + yes_button=QMessageBox.Apply): + self.commit(db, id_) + db.commit() + self.original_val = self.current_val + else: + self.current_val = self.original_val + d = TagEditor(self, db, id_) + if d.exec_() == TagEditor.Accepted: + self.current_val = d.tags + self.update_items_cache(db.all_tags()) + + + def commit(self, db, id_): + db.set_tags(id_, self.current_val, notify=False, commit=False) + return True + +# }}} + +class ISBNEdit(QLineEdit): # {{{ + LABEL = _('IS&BN:') + + def __init__(self, parent): + QLineEdit.__init__(self, parent) + self.pat = re.compile(r'[^0-9a-zA-Z]') + self.textChanged.connect(self.validate) + + @dynamic_property + def current_val(self): + def fget(self): + return self.pat.sub('', unicode(self.text()).strip()) + def fset(self, val): + if not val: + val = '' + self.setText(val.strip()) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = db.isbn(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_isbn(id_, self.current_val, notify=False, commit=False) + return True + + def validate(self, *args): + isbn = self.current_val + tt = _('This ISBN number is valid') + if not isbn: + col = 'rgba(0,255,0,0%)' + elif check_isbn(isbn) is not None: + col = 'rgba(0,255,0,20%)' + else: + col = 'rgba(255,0,0,20%)' + tt = _('This ISBN number is invalid') + self.setToolTip(tt) + self.setStyleSheet('QLineEdit { background-color: %s }'%col) + +# }}} + +class PublisherEdit(EnComboBox): # {{{ + LABEL = _('&Publisher:') + + def __init__(self, parent): + EnComboBox.__init__(self, parent) + self.setSizeAdjustPolicy( + self.AdjustToMinimumContentsLengthWithIcon) + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.currentText()).strip() + + def fset(self, val): + if not val: + val = '' + self.setEditText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + all_publishers = db.all_publishers() + all_publishers.sort(key=lambda x : sort_key(x[1])) + publisher_id = db.publisher_id(id_, index_is_id=True) + idx, c = None, 0 + for i in all_publishers: + id, name = i + if id == publisher_id: + idx = c + self.addItem(name) + c += 1 + + self.setEditText('') + if idx is not None: + self.setCurrentIndex(idx) + + def commit(self, db, id_): + db.set_publisher(id_, self.current_val, notify=False, commit=False) + return True + +# }}} + +class DateEdit(QDateEdit): # {{{ + + TOOLTIP = '' + LABEL = _('&Date:') + FMT = 'd MMM yyyy' + ATTR = 'timestamp' + + def __init__(self, parent): + QDateEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + fmt = self.FMT + if fmt is None: + fmt = tweaks['gui_pubdate_display_format'] + if fmt is None: + fmt = 'MMM yyyy' + self.setDisplayFormat(fmt) + self.setCalendarPopup(True) + self.setMinimumDate(UNDEFINED_QDATE) + self.setSpecialValueText(_('Undefined')) + self.clear_button = QToolButton(parent) + self.clear_button.setIcon(QIcon(I('trash.png'))) + self.clear_button.setToolTip(_('Clear date')) + self.clear_button.clicked.connect(self.reset_date) + + def reset_date(self, *args): + self.current_val = None + + @dynamic_property + def current_val(self): + def fget(self): + return qt_to_dt(self.date()) + def fset(self, val): + if val is None: + val = UNDEFINED_DATE + self.setDate(QDate(val.year, val.month, val.day)) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = getattr(db, self.ATTR)(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + if self.changed: + getattr(db, 'set_'+self.ATTR)(id_, self.current_val, commit=False, + notify=False) + return True + + @property + def changed(self): + o, c = self.original_val, self.current_val + return o.year != c.year or o.month != c.month or o.day != c.day + +class PubdateEdit(DateEdit): + LABEL = _('Publishe&d:') + FMT = None + ATTR = 'pubdate' + +# }}} diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b9fae51789..32fa6ea4f3 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -5,625 +5,22 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import textwrap, re, os +import os +from functools import partial -from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ - QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ - QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter +from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ + QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ + QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \ + QSizePolicy -from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog -from calibre.utils.icu import sort_key +from calibre.ebooks.metadata import authors_to_string, string_to_authors +from calibre.gui2 import ResizableDialog, error_dialog, gprefs +from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ + AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ + RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ + BuddyLabel, DateEdit, PubdateEdit +from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.utils.config import tweaks -from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList, ImageView -from calibre.ebooks.metadata import title_sort, authors_to_string, \ - string_to_authors -from calibre.utils.date import local_tz -from calibre import strftime -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.customize.ui import run_plugins_on_import -from calibre.utils.date import utcfromtimestamp - - -''' -The interface common to all widgets used to set basic metadata -class BasicMetadataWidget(object): - - LABEL = "label text" - - def initialize(self, db, id_): - pass - - def commit(self, db, id_): - return True - - @dynamic_property - def current_val(self): - # Present in most but not all basic metadata widgets - def fget(self): - return None - def fset(self, val): - pass - return property(fget=fget, fset=fset) -''' - -# Title {{{ -class TitleEdit(EnLineEdit): - - TITLE_ATTR = 'title' - COMMIT = True - TOOLTIP = _('Change the title of this book') - LABEL = _('&Title:') - - def __init__(self, parent): - self.dialog = parent - EnLineEdit.__init__(self, parent) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - - def get_default(self): - return _('Unknown') - - def initialize(self, db, id_): - title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True) - self.current_val = title - self.original_val = self.current_val - - def commit(self, db, id_): - title = self.current_val - if self.COMMIT: - getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False) - else: - getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False, - commit=False) - return True - - @dynamic_property - def current_val(self): - - def fget(self): - title = unicode(self.text()).strip() - if not title: - title = self.get_default() - return title - - def fset(self, val): - if hasattr(val, 'strip'): - val = val.strip() - if not val: - val = self.get_default() - self.setText(val) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - -class TitleSortEdit(TitleEdit): - - TITLE_ATTR = 'title_sort' - COMMIT = False - TOOLTIP = _('Specify how this book should be sorted when by title.' - ' For example, The Exorcist might be sorted as Exorcist, The.') - LABEL = _('Title &sort:') - - def __init__(self, parent, title_edit, autogen_button): - TitleEdit.__init__(self, parent) - self.title_edit = title_edit - - base = self.TOOLTIP - ok_tooltip = '

' + textwrap.fill(base+'

'+ - _(' The green color indicates that the current ' - 'title sort matches the current title')) - bad_tooltip = '

'+textwrap.fill(base + '

'+ - _(' The red color warns that the current ' - 'title sort does not match the current title. ' - 'No action is required if this is what you want.')) - self.tooltips = (ok_tooltip, bad_tooltip) - - self.title_edit.textChanged.connect(self.update_state) - self.textChanged.connect(self.update_state) - - autogen_button.clicked.connect(self.auto_generate) - self.update_state() - - def update_state(self, *args): - ts = title_sort(self.title_edit.current_val) - normal = ts == self.current_val - if normal: - col = 'rgb(0, 255, 0, 20%)' - else: - col = 'rgb(255, 0, 0, 20%)' - self.setStyleSheet('QLineEdit { color: black; ' - 'background-color: %s; }'%col) - tt = self.tooltips[0 if normal else 1] - self.setToolTip(tt) - self.setWhatsThis(tt) - - def auto_generate(self, *args): - self.current_val = title_sort(self.title_edit.current_val) - -# }}} - -# Authors {{{ -class AuthorsEdit(CompleteComboBox): - - TOOLTIP = '' - LABEL = _('&Author(s):') - - def __init__(self, parent): - self.dialog = parent - CompleteComboBox.__init__(self, parent) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - self.setEditable(True) - self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) - - def get_default(self): - return _('Unknown') - - def initialize(self, db, id_): - all_authors = db.all_authors() - all_authors.sort(key=lambda x : sort_key(x[1])) - for i in all_authors: - id, name = i - name = [name.strip().replace('|', ',') for n in name.split(',')] - self.addItem(authors_to_string(name)) - - self.set_separator('&') - self.set_space_before_sep(True) - self.update_items_cache(db.all_author_names()) - - au = db.authors(id_, index_is_id=True) - if not au: - au = _('Unknown') - self.current_val = [a.strip().replace('|', ',') for a in au.split(',')] - self.original_val = self.current_val - - def commit(self, db, id_): - authors = self.current_val - db.set_authors(id_, authors, notify=False) - return True - - @dynamic_property - def current_val(self): - - def fget(self): - au = unicode(self.text()).strip() - if not au: - au = self.get_default() - return string_to_authors(au) - - def fset(self, val): - if not val: - val = [self.get_default()] - self.setEditText(' & '.join([x.strip() for x in val])) - self.lineEdit().setCursorPosition(0) - - - return property(fget=fget, fset=fset) - -class AuthorSortEdit(EnLineEdit): - - TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' - 'For example Charles Dickens should be sorted as Dickens, ' - 'Charles.\nIf the box is colored green, then text matches ' - 'the individual author\'s sort strings. If it is colored ' - 'red, then the authors and this text do not match.') - LABEL = _('Author s&ort:') - - def __init__(self, parent, authors_edit, autogen_button, db): - EnLineEdit.__init__(self, parent) - self.authors_edit = authors_edit - self.db = db - - base = self.TOOLTIP - ok_tooltip = '

' + textwrap.fill(base+'

'+ - _(' The green color indicates that the current ' - 'author sort matches the current author')) - bad_tooltip = '

'+textwrap.fill(base + '

'+ - _(' The red color indicates that the current ' - 'author sort does not match the current author. ' - 'No action is required if this is what you want.')) - self.tooltips = (ok_tooltip, bad_tooltip) - - self.authors_edit.editTextChanged.connect(self.update_state) - self.textChanged.connect(self.update_state) - - autogen_button.clicked.connect(self.auto_generate) - self.update_state() - - @dynamic_property - def current_val(self): - - def fget(self): - return unicode(self.text()).strip() - - def fset(self, val): - if not val: - val = '' - self.setText(val.strip()) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - - def update_state(self, *args): - au = unicode(self.authors_edit.text()) - au = re.sub(r'\s+et al\.$', '', au) - au = self.db.author_sort_from_authors(string_to_authors(au)) - - normal = au == self.current_val - if normal: - col = 'rgb(0, 255, 0, 20%)' - else: - col = 'rgb(255, 0, 0, 20%)' - self.setStyleSheet('QLineEdit { color: black; ' - 'background-color: %s; }'%col) - tt = self.tooltips[0 if normal else 1] - self.setToolTip(tt) - self.setWhatsThis(tt) - - def auto_generate(self, *args): - au = unicode(self.authors_edit.text()) - au = re.sub(r'\s+et al\.$', '', au) - authors = string_to_authors(au) - self.current_val = self.db.author_sort_from_authors(authors) - - def initialize(self, db, id_): - self.current_val = db.author_sort(id_, index_is_id=True) - - def commit(self, db, id_): - aus = self.current_val - db.set_author_sort(id_, aus, notify=False, commit=False) - return True - -# }}} - -# Series {{{ -class SeriesEdit(EnComboBox): - - TOOLTIP = _('List of known series. You can add new series.') - LABEL = _('&Series:') - - def __init__(self, parent): - EnComboBox.__init__(self, parent) - self.dialog = parent - self.setSizeAdjustPolicy( - self.AdjustToMinimumContentsLengthWithIcon) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - self.setEditable(True) - - @dynamic_property - def current_val(self): - - def fget(self): - return unicode(self.currentText()).strip() - - def fset(self, val): - if not val: - val = '' - self.setEditText(val.strip()) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - all_series = db.all_series() - all_series.sort(key=lambda x : sort_key(x[1])) - series_id = db.series_id(id_, index_is_id=True) - idx, c = None, 0 - for i in all_series: - id, name = i - if id == series_id: - idx = c - self.addItem(name) - c += 1 - - self.lineEdit().setText('') - if idx is not None: - self.setCurrentIndex(idx) - self.original_val = self.current_val - - def commit(self, db, id_): - series = self.current_val - db.set_series(id_, series, notify=False, commit=True) - return True - -class SeriesIndexEdit(QDoubleSpinBox): - - TOOLTIP = '' - LABEL = _('&Number:') - - def __init__(self, parent, series_edit): - QDoubleSpinBox.__init__(self, parent) - self.dialog = parent - self.db = self.original_series_name = None - self.setMaximum(1000000) - self.series_edit = series_edit - series_edit.currentIndexChanged.connect(self.enable) - series_edit.editTextChanged.connect(self.enable) - series_edit.lineEdit().editingFinished.connect(self.increment) - self.enable() - - def enable(self, *args): - self.setEnabled(bool(self.series_edit.current_val)) - - @dynamic_property - def current_val(self): - - def fget(self): - return self.value() - - def fset(self, val): - if val is None: - val = 1.0 - val = float(val) - self.setValue(val) - - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - self.db = db - if self.series_edit.current_val: - val = db.series_index(id_, index_is_id=True) - else: - val = 1.0 - self.current_val = val - self.original_val = self.current_val - self.original_series_name = self.series_edit.original_val - - def commit(self, db, id_): - db.set_series_index(id_, self.current_val, notify=False, commit=False) - return True - - def increment(self): - if self.db is not None: - try: - series = self.series_edit.current_val - if series and series != self.original_series_name: - ns = 1.0 - if tweaks['series_index_auto_increment'] != 'const': - ns = self.db.get_next_series_num_for(series) - self.current_val = ns - self.original_series_name = series - except: - import traceback - traceback.print_exc() - - -# }}} - -class BuddyLabel(QLabel): # {{{ - - def __init__(self, buddy): - QLabel.__init__(self, buddy.LABEL) - self.setBuddy(buddy) - self.setAlignment(Qt.AlignRight|Qt.AlignVCenter) -# }}} - -class Format(QListWidgetItem): # {{{ - - def __init__(self, parent, ext, size, path=None, timestamp=None): - self.path = path - self.ext = ext - self.size = float(size)/(1024*1024) - text = '%s (%.2f MB)'%(self.ext.upper(), self.size) - QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), - text, parent, QListWidgetItem.UserType) - if timestamp is not None: - ts = timestamp.astimezone(local_tz) - t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple()) - text = _('Last modified: %s')%t - self.setToolTip(text) - self.setStatusTip(text) - -# }}} - -class FormatsManager(QWidget): # {{{ - - def __init__(self, parent): - QWidget.__init__(self, parent) - self.dialog = parent - self.changed = False - - self.l = l = QGridLayout() - self.setLayout(l) - self.cover_from_format_button = QToolButton(self) - self.cover_from_format_button.setToolTip( - _('Set the cover for the book from the selected format')) - self.cover_from_format_button.setIcon(QIcon(I('book.png'))) - self.cover_from_format_button.setIconSize(QSize(32, 32)) - - self.metadata_from_format_button = QToolButton(self) - self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) - self.metadata_from_format_button.setIconSize(QSize(32, 32)) - # TODO: Implement the *_from_format buttons - - self.add_format_button = QToolButton(self) - self.add_format_button.setIcon(QIcon(I('add_book.png'))) - self.add_format_button.setIconSize(QSize(32, 32)) - self.add_format_button.clicked.connect(self.add_format) - - self.remove_format_button = QToolButton(self) - self.remove_format_button.setIcon(QIcon(I('trash.png'))) - self.remove_format_button.setIconSize(QSize(32, 32)) - self.remove_format_button.clicked.connect(self.remove_format) - - self.formats = FormatList(self) - self.formats.setAcceptDrops(True) - self.formats.formats_dropped.connect(self.formats_dropped) - self.formats.delete_format.connect(self.remove_format) - self.formats.itemDoubleClicked.connect(self.show_format) - self.formats.setDragDropMode(self.formats.DropOnly) - self.formats.setIconSize(QSize(32, 32)) - self.formats.setMaximumWidth(200) - - l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) - l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1) - l.addWidget(self.add_format_button, 0, 2, 1, 1) - l.addWidget(self.remove_format_button, 2, 2, 1, 1) - l.addWidget(self.formats, 0, 1, 3, 1) - - - - def initialize(self, db, id_): - self.changed = False - exts = db.formats(id_, index_is_id=True) - if exts: - exts = exts.split(',') - for ext in exts: - if not ext: - ext = '' - size = db.sizeof_format(id_, ext, index_is_id=True) - timestamp = db.format_last_modified(id_, ext) - if size is None: - continue - Format(self.formats, ext, size, timestamp=timestamp) - - def commit(self, db, id_): - if not self.changed: - return True - old_extensions, new_extensions, paths = set(), set(), {} - for row in range(self.formats.count()): - fmt = self.formats.item(row) - ext, path = fmt.ext.lower(), fmt.path - if 'unknown' in ext.lower(): - ext = None - if path: - new_extensions.add(ext) - paths[ext] = path - else: - old_extensions.add(ext) - for ext in new_extensions: - db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False, - index_is_id=True) - db_extensions = set([f.lower() for f in db.formats(id_, - index_is_id=True).split(',')]) - extensions = new_extensions.union(old_extensions) - for ext in db_extensions: - if ext not in extensions: - db.remove_format(id_, ext, notify=False, index_is_id=True) - - self.changed = False - return True - - def add_format(self, *args): - files = choose_files(self, 'add formats dialog', - _("Choose formats for ") + - self.dialog.title.current_val, - [(_('Books'), BOOK_EXTENSIONS)]) - self._add_formats(files) - - def _add_formats(self, paths): - added = False - if not paths: - return added - bad_perms = [] - for _file in paths: - _file = os.path.abspath(_file) - if not os.access(_file, os.R_OK): - bad_perms.append(_file) - continue - - nfile = run_plugins_on_import(_file) - if nfile is not None: - _file = nfile - stat = os.stat(_file) - size = stat.st_size - ext = os.path.splitext(_file)[1].lower().replace('.', '') - timestamp = utcfromtimestamp(stat.st_mtime) - for row in range(self.formats.count()): - fmt = self.formats.item(row) - if fmt.ext.lower() == ext: - self.formats.takeItem(row) - break - Format(self.formats, ext, size, path=_file, timestamp=timestamp) - self.changed = True - added = True - if bad_perms: - error_dialog(self, _('No permission'), - _('You do not have ' - 'permission to read the following files:'), - det_msg='\n'.join(bad_perms), show=True) - - return added - - def formats_dropped(self, event, paths): - if self._add_formats(paths): - event.accept() - - def remove_format(self, *args): - rows = self.formats.selectionModel().selectedRows(0) - for row in rows: - self.formats.takeItem(row.row()) - self.changed = True - - def show_format(self, item, *args): - fmt = item.ext - self.dialog.view_format.emit(fmt) - -# }}} - -class Cover(ImageView): - - def __init__(self, parent): - ImageView.__init__(self, parent) - self._cdata = None - self.cover_changed.connect(self.set_pixmap_from_data) - - def set_pixmap_from_data(self, data): - if not data: - self.current_val = None - return - orig = self.current_val - self.current_val = data - if self.current_val is None: - error_dialog(self, _('Invalid cover'), - _('Could not change cover as the image is invalid.'), - show=True) - self.current_val = orig - - def initialize(self, db, id_): - self._cdata = None - self.current_val = db.cover(id_, index_is_id=True) - self.original_val = self.current_val - - @property - def changed(self): - return self.current_val != self.original_val - - @dynamic_property - def current_val(self): - def fget(self): - return self._cdata - def fset(self, cdata): - self._cdata = None - pm = QPixmap() - if cdata: - pm.loadFromData(cdata) - if pm.isNull(): - pm = QPixmap(I('default_cover.png')) - else: - self._cdata = cdata - self.setPixmap(pm) - tt = _('This book has no cover') - if self._cdata: - tt = _('Cover size: %dx%d pixels') % \ - (pm.width(), pm.height()) - self.setToolTip(tt) - - return property(fget=fget, fset=fset) - - def commit(self, db, id_): - if self.changed: - if self.current_val: - db.set_cover(id_, self.current_val, notify=False, commit=False) - else: - db.remove_cover(id_, notify=False, commit=False) - return True - - class MetadataSingleDialog(ResizableDialog): @@ -631,6 +28,7 @@ class MetadataSingleDialog(ResizableDialog): def __init__(self, db, parent=None): self.db = db + self.changed = set([]) ResizableDialog.__init__(self, parent) def setupUi(self, *args): # {{{ @@ -641,6 +39,14 @@ class MetadataSingleDialog(ResizableDialog): self) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) + self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'), + self) + self.next_button.clicked.connect(partial(self.do_one, delta=1)) + self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), + self) + self.button_box.addButton(self.prev_button, self.button_box.ActionRole) + self.button_box.addButton(self.next_button, self.button_box.ActionRole) + self.prev_button.clicked.connect(partial(self.do_one, delta=-1)) self.scroll_area = QScrollArea(self) self.scroll_area.setFrameShape(QScrollArea.NoFrame) @@ -659,13 +65,23 @@ class MetadataSingleDialog(ResizableDialog): self.create_basic_metadata_widgets() + if len(self.db.custom_column_label_map) == 0: + self.central_widget.tabBar().setVisible(False) + else: + self.create_custom_metadata_widgets() + + self.do_layout() + geom = gprefs.get('metasingle_window_geometry3', None) + if geom is not None: + self.restoreGeometry(bytes(geom)) # }}} def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] - # Title + self.title = TitleEdit(self) + self.title.textChanged.connect(self.update_window_title) self.deduce_title_sort_button = QToolButton(self) self.deduce_title_sort_button.setToolTip( _('Automatically create the title sort entry based on the current ' @@ -677,7 +93,6 @@ class MetadataSingleDialog(ResizableDialog): self.deduce_title_sort_button) self.basic_metadata_widgets.extend([self.title, self.title_sort]) - # Authors self.authors = AuthorsEdit(self) self.deduce_author_sort_button = QToolButton(self) self.deduce_author_sort_button.setToolTip(_( @@ -705,10 +120,62 @@ class MetadataSingleDialog(ResizableDialog): self.formats_manager = FormatsManager(self) self.basic_metadata_widgets.append(self.formats_manager) - + self.formats_manager.metadata_from_format_button.clicked.connect( + self.metadata_from_format) + self.formats_manager.cover_from_format_button.clicked.connect( + self.cover_from_format) self.cover = Cover(self) self.basic_metadata_widgets.append(self.cover) + self.comments = CommentsEdit(self) + self.basic_metadata_widgets.append(self.comments) + + self.rating = RatingEdit(self) + self.basic_metadata_widgets.append(self.rating) + + self.tags = TagsEdit(self) + self.tags_editor_button = QToolButton(self) + self.tags_editor_button.setToolTip(_('Open Tag Editor')) + self.tags_editor_button.setIcon(QIcon(I('chapters.png'))) + self.tags_editor_button.clicked.connect(self.tags_editor) + self.basic_metadata_widgets.append(self.tags) + + self.isbn = ISBNEdit(self) + self.basic_metadata_widgets.append(self.isbn) + + self.publisher = PublisherEdit(self) + self.basic_metadata_widgets.append(self.publisher) + + self.timestamp = DateEdit(self) + self.pubdate = PubdateEdit(self) + self.basic_metadata_widgets.extend([self.timestamp, self.pubdate]) + + self.fetch_metadata_button = QPushButton( + _('&Fetch metadata from server'), self) + self.fetch_metadata_button.clicked.connect(self.fetch_metadata) + font = self.fmb_font = QFont() + font.setBold(True) + self.fetch_metadata_button.setFont(font) + + + # }}} + + def create_custom_metadata_widgets(self): # {{{ + self.custom_metadata_widgets_parent = w = QWidget(self) + layout = QGridLayout() + w.setLayout(layout) + self.custom_metadata_widgets, self.__cc_spacers = \ + populate_metadata_page(layout, self.db, None, parent=w, bulk=False, + two_column=tweaks['metadata_single_use_2_cols_for_custom_fields']) + self.__custom_col_layouts = [layout] + ans = self.custom_metadata_widgets + for i in range(len(ans)-1): + if len(ans[i+1].widgets) == 2: + w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1]) + else: + w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0]) + for c in range(2, len(ans[i].widgets), 2): + w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1]) # }}} def do_layout(self): # {{{ @@ -720,7 +187,17 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].l = l = QVBoxLayout() self.tabs[0].tl = tl = QGridLayout() self.tabs[0].setLayout(l) + w = getattr(self, 'custom_metadata_widgets_parent', None) + if w is not None: + self.tabs.append(w) + self.central_widget.addTab(w, _('&Custom metadata')) l.addLayout(tl) + l.addItem(QSpacerItem(10, 15, QSizePolicy.Expanding, + QSizePolicy.Fixed)) + + sto = QWidget.setTabOrder + sto(self.button_box, self.fetch_metadata_button) + sto(self.fetch_metadata_button, self.title) def create_row(row, one, two, three, col=1, icon='forward.png'): ql = BuddyLabel(one) @@ -734,26 +211,96 @@ class MetadataSingleDialog(ResizableDialog): tl.addWidget(ql, row, col+3, 1, 1) self.labels.append(ql) tl.addWidget(three, row, col+4, 1, 1) + sto(one, two) + sto(two, three) tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1) create_row(0, self.title, self.deduce_title_sort_button, self.title_sort) + sto(self.title_sort, self.authors) create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort) + sto(self.author_sort, self.series) create_row(2, self.series, self.remove_unused_series_button, self.series_index, icon='trash.png') + sto(self.series_index, self.swap_title_author_button) tl.addWidget(self.formats_manager, 0, 6, 3, 1) self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.addWidget(self.cover) l.addWidget(self.splitter) + self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self) + gb.l = l = QGridLayout() + gb.setLayout(l) + sto(self.swap_title_author_button, self.cover.buttons[0]) + for i, b in enumerate(self.cover.buttons[:3]): + l.addWidget(b, 0, i, 1, 1) + sto(b, self.cover.buttons[i+1]) + gb.hl = QHBoxLayout() + for b in self.cover.buttons[3:]: + gb.hl.addWidget(b) + sto(self.cover.buttons[-2], self.cover.buttons[-1]) + l.addLayout(gb.hl, 1, 0, 1, 3) + self.tabs[0].middle = w = QWidget(self) + w.l = l = QGridLayout() + w.setLayout(w.l) + l.setMargin(0) + self.splitter.addWidget(w) + def create_row2(row, widget, button=None): + row += 1 + ql = BuddyLabel(widget) + l.addWidget(ql, row, 0, 1, 1) + l.addWidget(widget, row, 1, 1, 2 if button is None else 1) + if button is not None: + l.addWidget(button, row, 2, 1, 1) + if button is not None: + sto(widget, button) + + l.addWidget(gb, 0, 0, 1, 3) + self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding, + QSizePolicy.Expanding) + l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3) + sto(self.cover.buttons[-1], self.rating) + create_row2(1, self.rating) + sto(self.rating, self.tags) + create_row2(2, self.tags, self.tags_editor_button) + sto(self.tags_editor_button, self.isbn) + create_row2(3, self.isbn) + sto(self.isbn, self.timestamp) + create_row2(4, self.timestamp, self.timestamp.clear_button) + sto(self.timestamp.clear_button, self.pubdate) + create_row2(5, self.pubdate, self.pubdate.clear_button) + sto(self.pubdate.clear_button, self.publisher) + create_row2(6, self.publisher) + self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding, + QSizePolicy.Expanding) + l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3) + l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3) + + self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self) + gb.l = l = QVBoxLayout() + gb.setLayout(l) + l.addWidget(self.comments) + self.splitter.addWidget(gb) + # }}} - def __call__(self, id_, has_next=False, has_previous=False): - # TODO: Next and previous buttons + def __call__(self, id_): self.book_id = id_ for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) + for widget in self.custom_metadata_widgets: + widget.initialize(id_) + # Commented out as it doesn't play nice with Next, Prev buttons + #self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) + + + def update_window_title(self, *args): + title = self.title.current_val + if len(title) > 50: + title = title[:50] + u'\u2026' + self.setWindowTitle(_('Edit Meta Information') + ' - ' + + title) def swap_title_author(self, *args): title = self.title.current_val @@ -773,13 +320,155 @@ class MetadataSingleDialog(ResizableDialog): self.series.setCurrentIndex(i) break + def tags_editor(self, *args): + self.tags.edit(self.db, self.book_id) + + def metadata_from_format(self, *args): + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + if mi is not None: + self.update_from_mi(mi) + + def cover_from_format(self, *args): + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + if mi is None: + return + cdata = None + if mi.cover and os.access(mi.cover, os.R_OK): + cdata = open(mi.cover).read() + elif mi.cover_data[1] is not None: + cdata = mi.cover_data[1] + if cdata is None: + error_dialog(self, _('Could not read cover'), + _('Could not read cover from %s format')%ext).exec_() + return + orig = self.cover.current_val + self.cover.current_val = cdata + if self.cover.current_val is None: + self.cover.current_val = orig + return error_dialog(self, _('Could not read cover'), + _('The cover in the %s format is invalid')%ext, + show=True) + return + + def update_from_mi(self, mi): + if not mi.is_null('title'): + self.title.current_val = mi.title + if not mi.is_null('authors'): + self.authors.current_val = mi.authors + if not mi.is_null('author_sort'): + self.author_sort.current_val = mi.author_sort + if not mi.is_null('rating'): + try: + self.rating.current_val = mi.rating + except: + pass + if not mi.is_null('publisher'): + self.publisher.current_val = mi.publisher + if not mi.is_null('tags'): + self.tags.current_val = mi.tags + if not mi.is_null('isbn'): + self.isbn.current_val = mi.isbn + if not mi.is_null('pubdate'): + self.pubdate.current_val = mi.pubdate + if not mi.is_null('series') and mi.series.strip(): + self.series.current_val = mi.series + if mi.series_index is not None: + self.series_index.current_val = float(mi.series_index) + if mi.comments and mi.comments.strip(): + self.comments.current_val = mi.comments + + def fetch_metadata(self, *args): + pass # TODO: fetch metadata + + def apply_changes(self): + self.changed.add(self.book_id) + for widget in self.basic_metadata_widgets: + try: + if not widget.commit(self.db, self.book_id): + return False + except IOError, err: + if err.errno == 13: # Permission denied + import traceback + fname = err.filename if err.filename else 'file' + error_dialog(self, _('Permission denied'), + _('Could not open %s. Is it being used by another' + ' program?')%fname, det_msg=traceback.format_exc(), + show=True) + return False + raise + for widget in getattr(self, 'custom_metadata_widgets', []): + widget.commit(self.book_id) + + self.db.commit() + return True + + def accept(self): + self.save_state() + if not self.apply_changes(): + return + ResizableDialog.accept(self) + + def reject(self): + self.save_state() + ResizableDialog.reject(self) + + def save_state(self): + gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) + + def start(self, row_list, current_row, view_slot=None): + self.row_list = row_list + self.current_row = current_row + if view_slot is not None: + self.view_format.connect(view_slot) + self.do_one() + ret = self.exec_() + self.break_cycles() + return ret + + def do_one(self, delta=0): + self.current_row += delta + prev = next_ = None + if self.current_row > 0: + prev = self.db.title(self.row_list[self.current_row-1]) + if self.current_row < len(self.row_list) - 1: + next_ = self.db.title(self.row_list[self.current_row+1]) + + if next_ is not None: + tip = _('Save changes and edit the metadata of %s')%next_ + self.next_button.setToolTip(tip) + self.next_button.setVisible(next_ is not None) + if prev is not None: + tip = _('Save changes and edit the metadata of %s')%prev + self.prev_button.setToolTip(tip) + self.prev_button.setVisible(prev is not None) + self(self.db.id(self.row_list[self.current_row])) + + def break_cycles(self): + # Break any reference cycles that could prevent python + # from garbage collecting this dialog + def disconnect(signal): + try: + signal.disconnect() + except: + pass # Fails if view format was never connected + disconnect(self.view_format) + for b in ('next_button', 'prev_button'): + x = getattr(self, b, None) + if x is not None: + disconnect(x.clicked) + +def edit_metadata(db, row_list, current_row, parent=None, view_slot=None): + d = MetadataSingleDialog(db, parent) + d.start(row_list, current_row, view_slot=view_slot) + return d.changed if __name__ == '__main__': from PyQt4.Qt import QApplication app = QApplication([]) from calibre.library import db db = db() - d = MetadataSingleDialog(db) - d(db.data[0][0]) - d.exec_() + row_list = list(range(len(db.data))) + edit_metadata(db, row_list, 0) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index c6d069cc86..b33c059c9b 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -440,6 +440,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ except: import traceback traceback.print_exc() + olddb.break_cycles() if self.device_connected: self.set_books_in_library(self.booklists(), reset=True) self.refresh_ondevice() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 33593e93fe..3dc110c1c8 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -361,6 +361,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.refresh() self.last_update_check = self.last_modified() + def break_cycles(self): + self.data = self.field_metadata = self.prefs = self.listeners = None def initialize_database(self): metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read() diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 3e382c8f10..5ebe91bc76 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -441,7 +441,7 @@ menu, choose "Validate fonts". I downloaded the installer, but it is not working? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Best place to ask for more help is in the `forums `_. +Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Try rebooting your computer and running a registry cleaner like `Wise registry cleaner `_. Best place to ask for more help is in the `forums `_. My antivirus program claims |app| is a virus/trojan? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/trac/bzr_commit_plugin.py b/src/calibre/trac/bzr_commit_plugin.py index 6c36115cae..2f91804315 100644 --- a/src/calibre/trac/bzr_commit_plugin.py +++ b/src/calibre/trac/bzr_commit_plugin.py @@ -104,12 +104,12 @@ class cmd_commit(_cmd_commit): def close_bug(self, bug, action, url, config): print 'Closing bug #%s'% bug - nick = config.get_nickname() + #nick = config.get_nickname() suffix = config.get_user_option('bug_close_comment') if suffix is None: suffix = 'The fix will be in the next release.' action = action+'ed' - msg = '%s in branch %s. %s'%(action, nick, suffix) + msg = '%s in branch %s. %s'%(action, 'lp:calibre', suffix) msg = msg.replace('Fixesed', 'Fixed') server = xmlrpclib.ServerProxy(url) server.ticket.update(int(bug), msg,