diff --git a/Changelog.yaml b/Changelog.yaml index 4def62b6b2..6fc6714970 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -4,7 +4,7 @@ # for important features/bug fixes. # Also, each release can have new and improved recipes. -- version: 0.7.41 +- version: 0.7.42 date: 2011-01-21 new features: diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 86b0f56315..e5792aeff8 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = 'calibre' -__version__ = '0.7.41' +__version__ = '0.7.42' __author__ = "Kovid Goyal " import re diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index ed102ecc80..2a71ecd43b 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -1541,7 +1541,10 @@ class MobiWriter(object): exth.write(data) nrecs += 1 if term == 'rights' : - rights = unicode(oeb.metadata.rights[0]).encode('utf-8') + try: + rights = unicode(oeb.metadata.rights[0]).encode('utf-8') + except: + rights = 'Unknown' exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8)) exth.write(rights) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 04bc5284ed..c7f7d8b94a 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -12,7 +12,8 @@ from lxml.html import soupparser from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \ QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \ - QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog + QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog, \ + QHBoxLayout from PyQt4.QtWebKit import QWebView, QWebPage from calibre.ebooks.chardet import xml_to_unicode @@ -488,7 +489,7 @@ class Highlighter(QSyntaxHighlighter): class Editor(QWidget): # {{{ - def __init__(self, parent=None): + def __init__(self, parent=None, one_line_toolbar=False): QWidget.__init__(self, parent) self.toolbar1 = QToolBar(self) self.toolbar2 = QToolBar(self) @@ -508,9 +509,14 @@ class Editor(QWidget): # {{{ self.wyswyg.layout = l = QVBoxLayout(self.wyswyg) self.setLayout(self._layout) l.setContentsMargins(0, 0, 0, 0) - l.addWidget(self.toolbar1) - l.addWidget(self.toolbar2) - l.addWidget(self.toolbar3) + if one_line_toolbar: + tb = QHBoxLayout() + l.addLayout(tb) + else: + tb = l + tb.addWidget(self.toolbar1) + tb.addWidget(self.toolbar2) + tb.addWidget(self.toolbar3) l.addWidget(self.editor) self._layout.addWidget(self.tabs) self.tabs.addTab(self.wyswyg, _('Normal view')) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 96ea785ff2..dc85bad012 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -77,9 +77,9 @@ class TitleEdit(EnLineEdit): def commit(self, db, id_): title = self.current_val if self.COMMIT: - getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False) + getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False) else: - getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False, + getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False, commit=False) return True diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index c9c4a5d610..3b163d84f7 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -11,7 +11,7 @@ from functools import partial from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \ - QSizePolicy + QSizePolicy, QPalette, QFrame, QSize from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.gui2 import ResizableDialog, error_dialog, gprefs @@ -22,9 +22,11 @@ from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.utils.config import tweaks -class MetadataSingleDialog(ResizableDialog): +class MetadataSingleDialogBase(ResizableDialog): view_format = pyqtSignal(object) + cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields'] + one_line_comments_toolbar = False def __init__(self, db, parent=None): self.db = db @@ -65,9 +67,7 @@ class MetadataSingleDialog(ResizableDialog): self.create_basic_metadata_widgets() - if len(self.db.custom_column_label_map) == 0: - self.central_widget.tabBar().setVisible(False) - else: + if len(self.db.custom_column_label_map): self.create_custom_metadata_widgets() @@ -101,7 +101,7 @@ class MetadataSingleDialog(ResizableDialog): 'Using this button to create author sort will change author sort from' ' red to green.')) self.author_sort = AuthorSortEdit(self, self.authors, - self.deduce_author_sort_button, db) + self.deduce_author_sort_button, self.db) self.basic_metadata_widgets.extend([self.authors, self.author_sort]) self.swap_title_author_button = QToolButton(self) @@ -127,7 +127,7 @@ class MetadataSingleDialog(ResizableDialog): self.cover = Cover(self) self.basic_metadata_widgets.append(self.cover) - self.comments = CommentsEdit(self) + self.comments = CommentsEdit(self, self.one_line_comments_toolbar) self.basic_metadata_widgets.append(self.comments) self.rating = RatingEdit(self) @@ -166,19 +166,213 @@ class MetadataSingleDialog(ResizableDialog): 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']) + two_column=self.cc_two_column) 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): # {{{ + def set_custom_metadata_tab_order(self, before=None, after=None): # {{{ + sto = QWidget.setTabOrder + if getattr(self, 'custom_metadata_widgets', []): + ans = self.custom_metadata_widgets + for i in range(len(ans)-1): + if before is not None and i == 0: + pass# Do something + if len(ans[i+1].widgets) == 2: + sto(ans[i].widgets[-1], ans[i+1].widgets[1]) + else: + sto(ans[i].widgets[-1], ans[i+1].widgets[0]) + for c in range(2, len(ans[i].widgets), 2): + sto(ans[i].widgets[c-1], ans[i].widgets[c+1]) + if after is not None: + pass # Do something + # }}} + + def do_layout(self): + raise NotImplementedError() + + def __call__(self, id_): + self.book_id = id_ + for widget in self.basic_metadata_widgets: + widget.initialize(self.db, id_) + for widget in self.custom_metadata_widgets: + widget.initialize(id_) + # Commented out as it doesn't play nice with Next, Prev buttons + #self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) + + + # Miscellaneous interaction methods {{{ + def update_window_title(self, *args): + title = self.title.current_val + if len(title) > 50: + title = title[:50] + u'\u2026' + self.setWindowTitle(_('Edit Metadata') + ' - ' + + title) + + def swap_title_author(self, *args): + title = self.title.current_val + self.title.current_val = authors_to_string(self.authors.current_val) + self.authors.current_val = string_to_authors(title) + self.title_sort.auto_generate() + self.author_sort.auto_generate() + + def remove_unused_series(self, *args): + self.db.remove_unused_series() + idx = self.series.current_val + self.series.clear() + self.series.initialize(self.db, self.book_id) + if idx: + for i in range(self.series.count()): + if unicode(self.series.itemText(i)) == idx: + self.series.setCurrentIndex(i) + break + + def tags_editor(self, *args): + self.tags.edit(self.db, self.book_id) + + def metadata_from_format(self, *args): + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + if mi is not None: + self.update_from_mi(mi) + + def cover_from_format(self, *args): + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + if mi is None: + return + cdata = None + if mi.cover and os.access(mi.cover, os.R_OK): + cdata = open(mi.cover).read() + elif mi.cover_data[1] is not None: + cdata = mi.cover_data[1] + if cdata is None: + error_dialog(self, _('Could not read cover'), + _('Could not read cover from %s format')%ext).exec_() + return + orig = self.cover.current_val + self.cover.current_val = cdata + if self.cover.current_val is None: + self.cover.current_val = orig + return error_dialog(self, _('Could not read cover'), + _('The cover in the %s format is invalid')%ext, + show=True) + return + + def update_from_mi(self, mi): + if not mi.is_null('title'): + self.title.current_val = mi.title + if not mi.is_null('authors'): + self.authors.current_val = mi.authors + if not mi.is_null('author_sort'): + self.author_sort.current_val = mi.author_sort + if not mi.is_null('rating'): + try: + self.rating.current_val = mi.rating + except: + pass + if not mi.is_null('publisher'): + self.publisher.current_val = mi.publisher + if not mi.is_null('tags'): + self.tags.current_val = mi.tags + if not mi.is_null('isbn'): + self.isbn.current_val = mi.isbn + if not mi.is_null('pubdate'): + self.pubdate.current_val = mi.pubdate + if not mi.is_null('series') and mi.series.strip(): + self.series.current_val = mi.series + if mi.series_index is not None: + self.series_index.current_val = float(mi.series_index) + if mi.comments and mi.comments.strip(): + self.comments.current_val = mi.comments + + def fetch_metadata(self, *args): + pass # TODO: fetch metadata + # }}} + + def apply_changes(self): + self.changed.add(self.book_id) + for widget in self.basic_metadata_widgets: + try: + if not widget.commit(self.db, self.book_id): + return False + except IOError, err: + if err.errno == 13: # Permission denied + import traceback + fname = err.filename if err.filename else 'file' + error_dialog(self, _('Permission denied'), + _('Could not open %s. Is it being used by another' + ' program?')%fname, det_msg=traceback.format_exc(), + show=True) + return False + raise + for widget in getattr(self, 'custom_metadata_widgets', []): + widget.commit(self.book_id) + + self.db.commit() + return True + + def accept(self): + self.save_state() + if not self.apply_changes(): + return + ResizableDialog.accept(self) + + def reject(self): + self.save_state() + ResizableDialog.reject(self) + + def save_state(self): + gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) + + # Dialog use methods {{{ + 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) + # }}} + +class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ + + def do_layout(self): + if len(self.db.custom_column_label_map) == 0: + self.central_widget.tabBar().setVisible(False) self.central_widget.clear() self.tabs = [] self.labels = [] @@ -283,181 +477,143 @@ class MetadataSingleDialog(ResizableDialog): l.addWidget(self.comments) self.splitter.addWidget(gb) - # }}} + self.set_custom_metadata_tab_order() - def __call__(self, id_): - self.book_id = id_ - for widget in self.basic_metadata_widgets: - widget.initialize(self.db, id_) - for widget in self.custom_metadata_widgets: - widget.initialize(id_) - # Commented out as it doesn't play nice with Next, Prev buttons - #self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) +# }}} +class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{ - def update_window_title(self, *args): - title = self.title.current_val - if len(title) > 50: - title = title[:50] + u'\u2026' - self.setWindowTitle(_('Edit Metadata') + ' - ' + - title) + cc_two_column = False + one_line_comments_toolbar = True - def swap_title_author(self, *args): - title = self.title.current_val - self.title.current_val = authors_to_string(self.authors.current_val) - self.authors.current_val = string_to_authors(title) - self.title_sort.auto_generate() - self.author_sort.auto_generate() + def do_layout(self): + self.central_widget.clear() + self.tabs = [] + self.labels = [] + sto = QWidget.setTabOrder - def remove_unused_series(self, *args): - self.db.remove_unused_series() - idx = self.series.current_val - self.series.clear() - self.series.initialize(self.db, self.book_id) - if idx: - for i in range(self.series.count()): - if unicode(self.series.itemText(i)) == idx: - self.series.setCurrentIndex(i) - break + self.tabs.append(QWidget(self)) + self.central_widget.addTab(self.tabs[0], _("&Metadata")) + self.tabs[0].l = QGridLayout() + self.tabs[0].setLayout(self.tabs[0].l) - def tags_editor(self, *args): - self.tags.edit(self.db, self.book_id) + self.tabs.append(QWidget(self)) + self.central_widget.addTab(self.tabs[1], _("&Cover and formats")) + self.tabs[1].l = QGridLayout() + self.tabs[1].setLayout(self.tabs[1].l) - 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) + # Tab 0 + tab0 = self.tabs[0] - 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 + tl = QGridLayout() + gb = QGroupBox(_('&Basic metadata'), self.tabs[0]) + self.tabs[0].l.addWidget(gb, 0, 0, 1, 1) + gb.setLayout(tl) - 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 + sto(self.button_box, self.title) - def fetch_metadata(self, *args): - pass # TODO: fetch metadata + def create_row(row, widget, tab_to, button=None, icon=None, span=1): + ql = BuddyLabel(widget) + tl.addWidget(ql, row, 1, 1, 1) + tl.addWidget(widget, row, 2, 1, 1) + if button is not None: + tl.addWidget(button, row, 3, span, 1) + if icon is not None: + button.setIcon(QIcon(I(icon))) + if tab_to is not None: + if button is not None: + sto(widget, button) + sto(button, tab_to) + else: + sto(widget, tab_to) - def apply_changes(self): - self.changed.add(self.book_id) - for widget in self.basic_metadata_widgets: - try: - if not widget.commit(self.db, self.book_id): - return False - except IOError, err: - if err.errno == 13: # Permission denied - import traceback - fname = err.filename if err.filename else 'file' - error_dialog(self, _('Permission denied'), - _('Could not open %s. Is it being used by another' - ' program?')%fname, det_msg=traceback.format_exc(), - show=True) - return False - raise - for widget in getattr(self, 'custom_metadata_widgets', []): - widget.commit(self.book_id) + tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1) - self.db.commit() - return True + create_row(0, self.title, self.title_sort, + button=self.deduce_title_sort_button, span=2, + icon='auto_author_sort.png') + create_row(1, self.title_sort, self.authors) + create_row(2, self.authors, self.author_sort, + button=self.deduce_author_sort_button, + span=2, icon='auto_author_sort.png') + create_row(3, self.author_sort, self.series) + create_row(4, self.series, self.series_index, + button=self.remove_unused_series_button, icon='trash.png') + create_row(5, self.series_index, self.tags) + create_row(6, self.tags, self.rating, button=self.tags_editor_button) + create_row(7, self.rating, self.pubdate) + create_row(8, self.pubdate, self.publisher, + button=self.pubdate.clear_button, icon='trash.png') + create_row(9, self.publisher, self.timestamp) + create_row(10, self.timestamp, self.isbn, + button=self.timestamp.clear_button, icon='trash.png') + create_row(11, self.isbn, self.comments) + tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), + 12, 1, 1 ,1) - def accept(self): - self.save_state() - if not self.apply_changes(): - return - ResizableDialog.accept(self) + w = getattr(self, 'custom_metadata_widgets_parent', None) + if w is not None: + gb = QGroupBox(_('C&ustom metadata'), tab0) + gbl = QVBoxLayout() + gb.setLayout(gbl) + sr = QScrollArea(tab0) + sr.setWidgetResizable(True) + sr.setBackgroundRole(QPalette.Base) + sr.setFrameStyle(QFrame.NoFrame) + sr.setWidget(w) + gbl.addWidget(sr) + self.tabs[0].l.addWidget(gb, 0, 1, 1, 1) + sto(self.isbn, gb) - def reject(self): - self.save_state() - ResizableDialog.reject(self) + w = QGroupBox(_('&Comments'), tab0) + sp = QSizePolicy() + sp.setVerticalStretch(10) + sp.setHorizontalPolicy(QSizePolicy.Expanding) + sp.setVerticalPolicy(QSizePolicy.Expanding) + w.setSizePolicy(sp) + l = QHBoxLayout() + w.setLayout(l) + l.addWidget(self.comments) + tab0.l.addWidget(w, 1, 0, 1, 2) - def save_state(self): - gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) + # Tab 1 + tab1 = self.tabs[1] - 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 + wsp = QWidget(tab1) + wgl = QVBoxLayout() + wsp.setLayout(wgl) - 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]) + # right-hand side of splitter + gb = QGroupBox(_('Change cover'), tab1) + 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]) + hl = QHBoxLayout() + for b in self.cover.buttons[3:]: + hl.addWidget(b) + sto(self.cover.buttons[-2], self.cover.buttons[-1]) + l.addLayout(hl, 1, 0, 1, 3) + wgl.addWidget(gb) + wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, + QSizePolicy.Expanding)) + wgl.addWidget(self.fetch_metadata_button) + wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, + QSizePolicy.Expanding)) + wgl.addWidget(self.formats_manager) - 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])) + self.splitter = QSplitter(Qt.Horizontal, tab1) + tab1.l.addWidget(self.splitter) + self.splitter.addWidget(self.cover) + self.splitter.addWidget(wsp) + + self.formats_manager.formats.setMaximumWidth(10000) + self.formats_manager.formats.setIconSize(QSize(64, 64)) + +# }}} - 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) @@ -467,8 +623,8 @@ def edit_metadata(db, row_list, current_row, parent=None, view_slot=None): if __name__ == '__main__': from PyQt4.Qt import QApplication app = QApplication([]) - from calibre.library import db - db = db() + from calibre.library import db as db_ + db = db_() row_list = list(range(len(db.data))) edit_metadata(db, row_list, 0)