From ad521ab6d3a5cdfc78118151244b6107d870b360 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 09:55:36 -0700 Subject: [PATCH 01/25] cover manip controls --- src/calibre/gui2/metadata/single.py | 108 +++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b9fae51789..1575702918 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -10,10 +10,10 @@ import textwrap, re, os from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter + QSplitter, QPushButton, QGroupBox, QHBoxLayout from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog + choose_files, error_dialog, choose_images from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ @@ -569,9 +569,99 @@ 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 @@ -747,6 +837,20 @@ class MetadataSingleDialog(ResizableDialog): 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) + for i, b in enumerate(self.cover.buttons[:3]): + l.addWidget(b, 0, i, 1, 1) + gb.hl = QHBoxLayout() + for b in self.cover.buttons[3:]: + gb.hl.addWidget(b) + l.addLayout(gb.hl, 1, 0, 1, 3) + self.tabs[0].middle = w = QWidget(self) + w.l = l = QGridLayout() + w.setLayout(w.l) + l.addWidget(gb, 0, 0, 1, 3) + self.splitter.addWidget(w) # }}} def __call__(self, id_, has_next=False, has_previous=False): From cdee30ffd39cd25746810b1af5ed341c54eb675e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 10:26:02 -0700 Subject: [PATCH 02/25] And we have comments --- src/calibre/gui2/metadata/single.py | 38 +++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 1575702918..9cffe2ee55 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -25,7 +25,8 @@ 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 ''' The interface common to all widgets used to set basic metadata @@ -565,7 +566,7 @@ class FormatsManager(QWidget): # {{{ # }}} -class Cover(ImageView): +class Cover(ImageView): # {{{ def __init__(self, parent): ImageView.__init__(self, parent) @@ -713,7 +714,30 @@ class Cover(ImageView): 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 MetadataSingleDialog(ResizableDialog): @@ -799,6 +823,9 @@ class MetadataSingleDialog(ResizableDialog): self.cover = Cover(self) self.basic_metadata_widgets.append(self.cover) + self.comments = CommentsEdit(self) + self.basic_metadata_widgets.append(self.comments) + # }}} def do_layout(self): # {{{ @@ -851,6 +878,13 @@ class MetadataSingleDialog(ResizableDialog): w.setLayout(w.l) l.addWidget(gb, 0, 0, 1, 3) self.splitter.addWidget(w) + + self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), 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): From a6bded4378460a6443f92e2d07719d63855a32c0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 11:11:26 -0700 Subject: [PATCH 03/25] Fix #8475 (calibre not finding device) --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 277070020b..1b5cbe4bed 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] }, From f573d07e819fe6f557465dd94712492774520ba9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 11:16:03 -0700 Subject: [PATCH 04/25] And we have tags --- src/calibre/gui2/dialogs/metadata_single.py | 2 +- src/calibre/gui2/dialogs/tag_editor.py | 4 +- src/calibre/gui2/metadata/single.py | 135 +++++++++++++++++++- src/calibre/manual/faq.rst | 2 +- 4 files changed, 133 insertions(+), 10 deletions(-) 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/metadata/single.py b/src/calibre/gui2/metadata/single.py index 9cffe2ee55..f531e62fde 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -10,14 +10,15 @@ import textwrap, re, os from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter, QPushButton, QGroupBox, QHBoxLayout + QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \ + QMessageBox from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog, choose_images + choose_files, error_dialog, choose_images, question_dialog from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList, ImageView + EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors from calibre.utils.date import local_tz @@ -27,6 +28,7 @@ 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 @@ -739,6 +741,104 @@ class CommentsEdit(Editor): # {{{ 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 MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -778,7 +878,7 @@ class MetadataSingleDialog(ResizableDialog): def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] - # Title + self.title = TitleEdit(self) self.deduce_title_sort_button = QToolButton(self) self.deduce_title_sort_button.setToolTip( @@ -791,7 +891,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(_( @@ -826,6 +925,17 @@ class MetadataSingleDialog(ResizableDialog): 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) + + # }}} def do_layout(self): # {{{ @@ -876,8 +986,18 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].middle = w = QWidget(self) w.l = l = QGridLayout() w.setLayout(w.l) - l.addWidget(gb, 0, 0, 1, 3) + l.setMargin(0) self.splitter.addWidget(w) + def create_row2(row, widget, button=None): + 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) + + l.addWidget(gb, 0, 0, 1, 3) + create_row2(1, self.rating) + create_row2(2, self.tags, self.tags_editor_button) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() @@ -911,6 +1031,9 @@ class MetadataSingleDialog(ResizableDialog): self.series.setCurrentIndex(i) break + def tags_editor(self, *args): + self.tags.edit(self.db, self.book_id) + if __name__ == '__main__': from PyQt4.Qt import QApplication 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? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 1820832fa8ae0bcb89c800595de9adc4ed52164b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 11:59:19 -0700 Subject: [PATCH 05/25] Fix #8477 (Series/Sequence Info no longer being downloaded) --- src/calibre/__init__.py | 8 +++++--- src/calibre/ebooks/metadata/library_thing.py | 21 ++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) 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/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) From 5f9fcaa1882436f3bd995bf330e5281048c3db58 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 13:15:53 -0700 Subject: [PATCH 06/25] Fix #8479 (Updated recipe for Blic) --- resources/recipes/blic.recipe | 44 ++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) 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 From cf4e47fcf8d7304ed66684f245df9aa460714fde Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 14:56:56 -0700 Subject: [PATCH 07/25] isbn added --- src/calibre/gui2/metadata/single.py | 48 +++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index f531e62fde..daae579334 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -11,7 +11,7 @@ from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \ - QMessageBox + QMessageBox, QLineEdit from calibre.gui2 import ResizableDialog, file_icon_provider, \ choose_files, error_dialog, choose_images, question_dialog @@ -20,7 +20,7 @@ from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.ebooks.metadata import title_sort, authors_to_string, \ - string_to_authors + string_to_authors, check_isbn from calibre.utils.date import local_tz from calibre import strftime from calibre.ebooks import BOOK_EXTENSIONS @@ -839,6 +839,47 @@ class TagsEdit(CompleteLineEdit): # {{{ # }}} +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 MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -935,6 +976,8 @@ class MetadataSingleDialog(ResizableDialog): 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) # }}} @@ -998,6 +1041,7 @@ class MetadataSingleDialog(ResizableDialog): l.addWidget(gb, 0, 0, 1, 3) create_row2(1, self.rating) create_row2(2, self.tags, self.tags_editor_button) + create_row2(3, self.isbn) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() From daeaa718123a75157b2053d0b95966ebbfdf5245 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 15:04:40 -0700 Subject: [PATCH 08/25] publisher added --- src/calibre/gui2/metadata/single.py | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index daae579334..de59e8075d 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -880,6 +880,50 @@ class ISBNEdit(QLineEdit): # {{{ # }}} +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 MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -979,6 +1023,9 @@ class MetadataSingleDialog(ResizableDialog): self.isbn = ISBNEdit(self) self.basic_metadata_widgets.append(self.isbn) + self.publisher = PublisherEdit(self) + self.basic_metadata_widgets.append(self.publisher) + # }}} def do_layout(self): # {{{ @@ -1042,6 +1089,7 @@ class MetadataSingleDialog(ResizableDialog): create_row2(1, self.rating) create_row2(2, self.tags, self.tags_editor_button) create_row2(3, self.isbn) + create_row2(4, self.publisher) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() From 631bd5d63d671ad73d912e37ec6e4e045366c217 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 15:14:17 -0700 Subject: [PATCH 09/25] ... --- src/calibre/gui2/metadata/basic_widgets.py | 927 +++++++++++++++++++++ src/calibre/gui2/metadata/single.py | 925 +------------------- 2 files changed, 936 insertions(+), 916 deletions(-) create mode 100644 src/calibre/gui2/metadata/basic_widgets.py diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py new file mode 100644 index 0000000000..eb162ac9d5 --- /dev/null +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -0,0 +1,927 @@ +#!/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, \ + 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 +from calibre.ebooks.metadata import title_sort, authors_to_string, \ + string_to_authors, check_isbn +from calibre.gui2 import file_icon_provider, \ + choose_files, error_dialog, choose_images, question_dialog +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 +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)) + # 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.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 + +# }}} + + diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index de59e8075d..2256816091 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -5,924 +5,17 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import textwrap, re, os -from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ - QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ - QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \ - QMessageBox, QLineEdit +from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \ + QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \ + QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox -from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog, choose_images, question_dialog -from calibre.utils.icu import sort_key -from calibre.utils.config import tweaks -from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList, ImageView, CompleteLineEdit -from calibre.ebooks.metadata import title_sort, authors_to_string, \ - string_to_authors, check_isbn -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 -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)) - # 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.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 - -# }}} +from calibre.ebooks.metadata import authors_to_string, string_to_authors +from calibre.gui2 import ResizableDialog +from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ + AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ + RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ + BuddyLabel class MetadataSingleDialog(ResizableDialog): From 96be6e90351041cac47cb5f17efc4fd1cd9d672b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 16:03:54 -0700 Subject: [PATCH 10/25] Basic metadata widgets layout complete --- src/calibre/gui2/metadata/basic_widgets.py | 67 ++++++++++++++++++++-- src/calibre/gui2/metadata/single.py | 35 +++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index eb162ac9d5..5d37e854da 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -7,11 +7,10 @@ __docformat__ = 'restructuredtext en' import textwrap, re, os -from PyQt4.Qt import Qt, \ +from PyQt4.Qt import Qt, QDateEdit, QDate, \ QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ - QPushButton, QSpinBox, \ - QMessageBox, QLineEdit + QPushButton, QSpinBox, QMessageBox, QLineEdit from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ EnComboBox, FormatList, ImageView, CompleteLineEdit @@ -19,9 +18,9 @@ from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors, check_isbn -from calibre.gui2 import file_icon_provider, \ +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 +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 @@ -924,4 +923,62 @@ class PublisherEdit(EnComboBox): # {{{ # }}} +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 2256816091..730e5f10b6 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -6,16 +6,17 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \ - QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \ - QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox +from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ + QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ + QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \ + QSizePolicy from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.gui2 import ResizableDialog from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ - BuddyLabel + BuddyLabel, DateEdit, PubdateEdit class MetadataSingleDialog(ResizableDialog): @@ -119,6 +120,17 @@ class MetadataSingleDialog(ResizableDialog): 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 do_layout(self): # {{{ @@ -172,6 +184,7 @@ class MetadataSingleDialog(ResizableDialog): 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) @@ -179,10 +192,19 @@ class MetadataSingleDialog(ResizableDialog): l.addWidget(button, row, 2, 1, 1) 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) create_row2(1, self.rating) create_row2(2, self.tags, self.tags_editor_button) create_row2(3, self.isbn) - create_row2(4, self.publisher) + create_row2(4, self.timestamp, self.timestamp.clear_button) + create_row2(5, self.pubdate, self.pubdate.clear_button) + 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(_('&Comments'), self) gb.l = l = QVBoxLayout() @@ -219,6 +241,9 @@ class MetadataSingleDialog(ResizableDialog): def tags_editor(self, *args): self.tags.edit(self.db, self.book_id) + def fetch_metadata(self, *args): + pass # TODO: fetch metadata + if __name__ == '__main__': from PyQt4.Qt import QApplication From d3bd5b07e8268d1ff3b79f15cae60b5e9efa87d4 Mon Sep 17 00:00:00 2001 From: ldolse Date: Fri, 21 Jan 2011 08:35:14 +0800 Subject: [PATCH 11/25] false positive tuning in txt input and dehyphenate --- src/calibre/ebooks/conversion/preprocess.py | 4 ++++ src/calibre/ebooks/txt/processor.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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/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: From a8aa6ef54aeeacab8a1f8045a0aff87db61e19ed Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 19:10:54 -0700 Subject: [PATCH 12/25] Implement accept in the new metadata dialog --- src/calibre/gui2/metadata/single.py | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 730e5f10b6..b53637d66a 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -12,7 +12,7 @@ from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QSizePolicy from calibre.ebooks.metadata import authors_to_string, string_to_authors -from calibre.gui2 import ResizableDialog +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, \ @@ -53,12 +53,16 @@ class MetadataSingleDialog(ResizableDialog): self.create_basic_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 = [] 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 ' @@ -144,6 +148,9 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].setLayout(l) l.addLayout(tl) + sto = QWidget.setTabOrder + sto(self.fetch_metadata_button, self.title) + def create_row(row, one, two, three, col=1, icon='forward.png'): ql = BuddyLabel(one) tl.addWidget(ql, row, col+0, 1, 1) @@ -156,13 +163,18 @@ 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) @@ -219,6 +231,16 @@ class MetadataSingleDialog(ResizableDialog): self.book_id = id_ for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) + 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 @@ -244,6 +266,30 @@ class MetadataSingleDialog(ResizableDialog): def fetch_metadata(self, *args): pass # TODO: fetch metadata + def accept(self): + for widget in self.basic_metadata_widgets: + try: + if not widget.commit(self.db, self.book_id): + return + except IOError, err: + if err.errno == 13: # Permission denied + import traceback + fname = err.filename if err.filename else 'file' + return error_dialog(self, _('Permission denied'), + _('Could not open %s. Is it being used by another' + ' program?')%fname, det_msg=traceback.format_exc(), + show=True) + raise + self.save_state() + ResizableDialog.accept(self) + + def reject(self): + self.save_state() + ResizableDialog.reject(self) + + def save_state(self): + gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) + if __name__ == '__main__': from PyQt4.Qt import QApplication From 5a2cbbc8c41dfe0e226b945c7496e2ba67b276b5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 19:14:20 -0700 Subject: [PATCH 13/25] Fix #8483 (Need Support for Archos 101) --- src/calibre/devices/android/driver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 1b5cbe4bed..a95e3c46fa 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -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' From 2e7967bd6e70670a3a57063bbc4647bb54c253f0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 19:45:59 -0700 Subject: [PATCH 14/25] Fix #7702 (Option to populate author from selected row when adding empty books) --- src/calibre/gui2/actions/add.py | 22 ++++-- src/calibre/gui2/dialogs/add_empty_book.py | 85 ++++++++++++++++++++++ 2 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 src/calibre/gui2/dialogs/add_empty_book.py 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/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_() From 3e7afccf7e8711a4e1f631861532ee1c31489007 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 20:28:58 -0700 Subject: [PATCH 15/25] ... --- src/calibre/gui2/actions/choose_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 6f4e883b1a..a2679c2482 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) From 96a369f1a55577b567190304cfe299c53e417ce6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 20:31:54 -0700 Subject: [PATCH 16/25] Link up get metadata and cover from format buttons --- src/calibre/gui2/metadata/basic_widgets.py | 34 +++++++++++- src/calibre/gui2/metadata/single.py | 64 +++++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 5d37e854da..96ea785ff2 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -15,9 +15,10 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \ from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.utils.icu import sort_key -from calibre.utils.config import tweaks +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 @@ -440,7 +441,6 @@ class FormatsManager(QWidget): # {{{ 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'))) @@ -565,6 +565,36 @@ class FormatsManager(QWidget): # {{{ 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): # {{{ diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b53637d66a..c7b5e7f99b 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -5,6 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import os from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ @@ -101,7 +102,10 @@ 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) @@ -263,6 +267,64 @@ class MetadataSingleDialog(ResizableDialog): 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 From 38e6416c3b69b6f98c35b34726259683cff0c800 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 20:58:32 -0700 Subject: [PATCH 17/25] completed implementation of new metadata dialog, except for downloading of metadata which is also going to be refactored, separately --- src/calibre/gui2/custom_column_widgets.py | 3 +- src/calibre/gui2/metadata/single.py | 51 ++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) 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/metadata/single.py b/src/calibre/gui2/metadata/single.py index c7b5e7f99b..26acf944e1 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -18,6 +18,8 @@ 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 class MetadataSingleDialog(ResizableDialog): @@ -53,6 +55,12 @@ 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: @@ -139,6 +147,25 @@ class MetadataSingleDialog(ResizableDialog): 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): # {{{ @@ -150,6 +177,10 @@ 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) sto = QWidget.setTabOrder @@ -235,6 +266,8 @@ class MetadataSingleDialog(ResizableDialog): 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_) self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) @@ -323,26 +356,34 @@ class MetadataSingleDialog(ResizableDialog): if mi.comments and mi.comments.strip(): self.comments.current_val = mi.comments - - def fetch_metadata(self, *args): pass # TODO: fetch metadata - def accept(self): + def apply_changes(self): for widget in self.basic_metadata_widgets: try: if not widget.commit(self.db, self.book_id): - return + return False except IOError, err: if err.errno == 13: # Permission denied import traceback fname = err.filename if err.filename else 'file' - return error_dialog(self, _('Permission denied'), + 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): From 6cc052260bf88895a6fbd171410c9fdd846e6059 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 22:27:12 -0700 Subject: [PATCH 18/25] Fix tab order --- src/calibre/gui2/metadata/single.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 26acf944e1..8b0e3da2d2 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -219,11 +219,14 @@ class MetadataSingleDialog(ResizableDialog): 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() @@ -237,23 +240,31 @@ class MetadataSingleDialog(ResizableDialog): 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(_('&Comments'), self) + self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self) gb.l = l = QVBoxLayout() gb.setLayout(l) l.addWidget(self.comments) From 5203777b2ab72372e33d16070c434d5074a3b562 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 23:09:22 -0700 Subject: [PATCH 19/25] next, previous buttons --- src/calibre/gui2/metadata/single.py | 70 ++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 8b0e3da2d2..99d10a156d 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -6,6 +6,7 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os +from functools import partial from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ @@ -27,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): # {{{ @@ -37,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) @@ -184,6 +194,7 @@ class MetadataSingleDialog(ResizableDialog): l.addLayout(tl) 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'): @@ -272,14 +283,14 @@ class MetadataSingleDialog(ResizableDialog): # }}} - 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_) - self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) + # 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): @@ -289,7 +300,6 @@ class MetadataSingleDialog(ResizableDialog): self.setWindowTitle(_('Edit Meta Information') + ' - ' + title) - def swap_title_author(self, *args): title = self.title.current_val self.title.current_val = authors_to_string(self.authors.current_val) @@ -371,6 +381,7 @@ class MetadataSingleDialog(ResizableDialog): 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): @@ -404,13 +415,58 @@ class MetadataSingleDialog(ResizableDialog): 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) From ca3f2dafc52223d79a1cc64422965a10c86f9db2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 23:23:12 -0700 Subject: [PATCH 20/25] ... --- src/calibre/gui2/metadata/single.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 99d10a156d..32fa6ea4f3 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -192,6 +192,8 @@ class MetadataSingleDialog(ResizableDialog): 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) From 229fe3e4d2c32830367d8ec5b2dc66bd567f0900 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 23:58:22 -0700 Subject: [PATCH 21/25] Add a confirmation when closing the add a custom news source dialog. Fixes #8460 (Add custom new source interface) --- src/calibre/gui2/dialogs/user_profiles.py | 7 +++++++ 1 file changed, 7 insertions(+) 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() From 7f2373c6bea1702447631ddcce12197dc2679da9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 21 Jan 2011 00:06:47 -0700 Subject: [PATCH 22/25] ... --- src/calibre/trac/bzr_commit_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From bad6cad84b6bc6d453f6efdf83400f4192206a24 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 21 Jan 2011 07:55:28 -0500 Subject: [PATCH 23/25] Fix bug #8422: RTF incorrect spacing between letters. --- src/calibre/ebooks/rtf/rtfml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 30f65e0ebac0afb344e96a1f15757f65287f1c98 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 21 Jan 2011 08:51:48 -0500 Subject: [PATCH 24/25] GUI: Regex Test, clear matched items when no regex is present. Speed up clearing by not running regex matching on empty patterns. --- src/calibre/gui2/convert/regex_builder.py | 2 ++ 1 file changed, 2 insertions(+) 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): From f22708691379aeac96911e68976b7cdd877fe6c2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 21 Jan 2011 09:46:50 -0700 Subject: [PATCH 25/25] Fix memory leak when switching libraries --- src/calibre/gui2/actions/choose_library.py | 7 +++++++ src/calibre/gui2/ui.py | 1 + src/calibre/library/database2.py | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index a2679c2482..930e5e29aa 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -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/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()