diff --git a/Changelog.yaml b/Changelog.yaml index 0e250ae534..310967cbbc 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -20,6 +20,44 @@ # new recipes: # - title: +- version: 1.10.0 + date: 2013-11-08 + + new features: + - title: "Conversion: Treat .docm the same as .docx files, ignoring any macros in the file." + tickets: [1247565] + + - title: "EPUB Output: Auto convert CMYK images to RGB. Works around the inability of Adobe Digital Editions to display CMYK images." + tickets: [1246710] + + - title: "Quickview: Add a checkbox to lock the quickview window so that it does not change while moving around in the main book list" + + bug fixes: + - title: "Fix number of marked books shown in the drop down menu of the Mark Books action not being updated when marked books are deleted (as opposed to being unmarked)." + tickets: [1248506] + + - title: "Book list: Preserve the current column when using Ctrl+Home or Ctrl+End shortcuts" + + - title: "Booklist: Fix using Page Up/Down keys moving book list by one row too many." + tickets: [1248109] + + - title: "Metadata download: Do not auto trim downloaded covers as trimming can sometimes have undesirable effects." + + - title: "Template language: Fix zero valued series indices not formatting correctly" + tickets: [1247348] + + - title: "Fix a regression in 1.9 that broke bibtex catalog creation" + + - title: "Quickview: Auto-close the quickview window when changing libraries. You have to open it again in the new library." + + - title: "Linux binary build: Update the bundled version of zlib to fix library compatibility on some newer linux distros that have libpng16 compiled to require newer zlib versions" + tickets: [1247315] + + improved recipes: + - New York Review of Books + - Various Polish news sources + + - version: 1.9.0 date: 2013-11-01 diff --git a/imgsrc/view-refresh.svg b/imgsrc/view-refresh.svg new file mode 100644 index 0000000000..4ab18c929b --- /dev/null +++ b/imgsrc/view-refresh.svg @@ -0,0 +1,1566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/manual/plugin_examples/interface_demo/images/icon.png b/manual/plugin_examples/interface_demo/images/icon.png index ad823e2ff4..7512b6ef07 100644 Binary files a/manual/plugin_examples/interface_demo/images/icon.png and b/manual/plugin_examples/interface_demo/images/icon.png differ diff --git a/manual/resources/logo.png b/manual/resources/logo.png index 158bc9d1b5..c952d762bf 100644 Binary files a/manual/resources/logo.png and b/manual/resources/logo.png differ diff --git a/recipes/argnoticias.recipe b/recipes/argnoticias.recipe new file mode 100644 index 0000000000..03e3627064 --- /dev/null +++ b/recipes/argnoticias.recipe @@ -0,0 +1,96 @@ + +__license__ = 'GPL v3' +__copyright__ = '2013, Darko Miletic ' + +''' +www.argnoticias.com +''' + +import time +from calibre import strftime +from calibre.web.feeds.recipes import BasicNewsRecipe + +class ArgNoticias(BasicNewsRecipe): + title = 'ARG Noticias' + __author__ = 'Darko Miletic' + description = 'Ultimas noticias de Argentina' + publisher = 'ARG Noticias' + category = 'news, politics, Argentina' + oldest_article = 2 + max_articles_per_feed = 100 + no_stylesheets = True + encoding = 'utf-8' + use_embedded_content = False + masthead_url = 'http://www.argnoticias.com/images/arg-logo-footer.png' + language = 'es_AR' + publication_type = 'newsportal' + INDEX = 'http://www.argnoticias.com' + extra_css = '' + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher': publisher + , 'language' : language + } + + keep_only_tags = [dict(name='div', attrs={'class':['itemHeader','itemBody','itemAuthorBlock']})] + + remove_tags = [ + dict(name=['object','link','base','iframe']), + dict(name='div', attrs={'class':['b2jsocial_parent','itemSocialSharing']}) + ] + + feeds = [ + (u'Politica' , u'http://www.argnoticias.com/index.php/politica' ) + ,(u'Economia' , u'http://www.argnoticias.com/index.php/economia' ) + ,(u'Sociedad' , u'http://www.argnoticias.com/index.php/sociedad' ) + ,(u'Mundo' , u'http://www.argnoticias.com/index.php/mundo' ) + ,(u'Deportes' , u'http://www.argnoticias.com/index.php/deportes' ) + ,(u'Espectaculos', u'http://www.argnoticias.com/index.php/espectaculos') + ,(u'Tendencias' , u'http://www.argnoticias.com/index.php/tendencias' ) + ] + + def parse_index(self): + totalfeeds = [] + lfeeds = self.get_feeds() + checker = [] + for feedobj in lfeeds: + feedtitle, feedurl = feedobj + self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl)) + articles = [] + soup = self.index_to_soup(feedurl) + for item in soup.findAll('div', attrs={'class':'Nota'}): + atag = item.find('a', attrs={'class':'moduleItemTitle'}) + ptag = item.find('div', attrs={'class':'moduleItemIntrotext'}) + url = self.INDEX + atag['href'] + title = self.tag_to_string(atag) + description = self.tag_to_string(ptag) + date = strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime()) + if url not in checker: + checker.append(url) + articles.append({ + 'title' :title + ,'date' :date + ,'url' :url + ,'description':description + }) + + for item in soup.findAll('li'): + atag = item.find('a', attrs={'class':'moduleItemTitle'}) + if atag: + ptag = item.find('div', attrs={'class':'moduleItemIntrotext'}) + url = self.INDEX + atag['href'] + title = self.tag_to_string(atag) + description = self.tag_to_string(ptag) + date = strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime()) + if url not in checker: + checker.append(url) + articles.append({ + 'title' :title + ,'date' :date + ,'url' :url + ,'description':description + }) + totalfeeds.append((feedtitle, articles)) + return totalfeeds diff --git a/recipes/icons/argnoticias.png b/recipes/icons/argnoticias.png new file mode 100644 index 0000000000..bdc5fc3290 Binary files /dev/null and b/recipes/icons/argnoticias.png differ diff --git a/recipes/wprost_rss.recipe b/recipes/wprost_rss.recipe index 7cd9d9ce5c..bc57e288d4 100644 --- a/recipes/wprost_rss.recipe +++ b/recipes/wprost_rss.recipe @@ -6,7 +6,6 @@ __copyright__ = '''2010, matek09, matek09@gmail.com Modified 2012, Artur Stachecki ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class Wprost(BasicNewsRecipe): title = u'Wprost (RSS)' diff --git a/resources/images/auto-reload.png b/resources/images/auto-reload.png new file mode 100644 index 0000000000..5590b33953 Binary files /dev/null and b/resources/images/auto-reload.png differ diff --git a/resources/images/embed-fonts.png b/resources/images/embed-fonts.png new file mode 100644 index 0000000000..fac5b5022d Binary files /dev/null and b/resources/images/embed-fonts.png differ diff --git a/resources/images/smarten-punctuation.png b/resources/images/smarten-punctuation.png new file mode 100644 index 0000000000..6f3cb92ed7 Binary files /dev/null and b/resources/images/smarten-punctuation.png differ diff --git a/resources/images/subset-fonts.png b/resources/images/subset-fonts.png new file mode 100644 index 0000000000..449f204158 Binary files /dev/null and b/resources/images/subset-fonts.png differ diff --git a/resources/images/view-refresh.png b/resources/images/view-refresh.png new file mode 100644 index 0000000000..0b08b2388e Binary files /dev/null and b/resources/images/view-refresh.png differ diff --git a/src/calibre/constants.py b/src/calibre/constants.py index a113f4b48a..fb7da67e9f 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -4,7 +4,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = u'calibre' -numeric_version = (1, 9, 0) +numeric_version = (1, 10, 0) __version__ = u'.'.join(map(unicode, numeric_version)) __author__ = u"Kovid Goyal " diff --git a/src/calibre/ebooks/oeb/polish/main.py b/src/calibre/ebooks/oeb/polish/main.py index c5a7d4db6d..19d3386c84 100644 --- a/src/calibre/ebooks/oeb/polish/main.py +++ b/src/calibre/ebooks/oeb/polish/main.py @@ -119,65 +119,68 @@ def update_metadata(ebook, new_opf): stream.truncate() stream.write(opf.render()) -def polish(file_map, opts, log, report): +def polish_one(ebook, opts, report): rt = lambda x: report('\n### ' + x) + jacket = None + + if opts.subset or opts.embed: + stats = StatsCollector(ebook, do_embed=opts.embed) + + if opts.opf: + rt(_('Updating metadata')) + update_metadata(ebook, opts.opf) + jacket = find_existing_jacket(ebook) + if jacket is not None: + replace_jacket(ebook, jacket) + report(_('Updated metadata jacket')) + report(_('Metadata updated\n')) + + if opts.cover: + rt(_('Setting cover')) + set_cover(ebook, opts.cover, report) + report('') + + if opts.jacket: + rt(_('Inserting metadata jacket')) + if jacket is None: + if add_or_replace_jacket(ebook): + report(_('Existing metadata jacket replaced')) + else: + report(_('Metadata jacket inserted')) + else: + report(_('Existing metadata jacket replaced')) + report('') + + if opts.remove_jacket: + rt(_('Removing metadata jacket')) + if remove_jacket(ebook): + report(_('Metadata jacket removed')) + else: + report(_('No metadata jacket found')) + report('') + + if opts.smarten_punctuation: + rt(_('Smartening punctuation')) + smarten_punctuation(ebook, report) + report('') + + if opts.embed: + rt(_('Embedding referenced fonts')) + embed_all_fonts(ebook, stats, report) + report('') + + if opts.subset: + rt(_('Subsetting embedded fonts')) + subset_all_fonts(ebook, stats.font_stats, report) + report('') + + +def polish(file_map, opts, log, report): st = time.time() for inbook, outbook in file_map.iteritems(): report(_('## Polishing: %s')%(inbook.rpartition('.')[-1].upper())) ebook = get_container(inbook, log) - jacket = None - - if opts.subset or opts.embed: - stats = StatsCollector(ebook, do_embed=opts.embed) - - if opts.opf: - rt(_('Updating metadata')) - update_metadata(ebook, opts.opf) - jacket = find_existing_jacket(ebook) - if jacket is not None: - replace_jacket(ebook, jacket) - report(_('Updated metadata jacket')) - report(_('Metadata updated\n')) - - if opts.cover: - rt(_('Setting cover')) - set_cover(ebook, opts.cover, report) - report('') - - if opts.jacket: - rt(_('Inserting metadata jacket')) - if jacket is None: - if add_or_replace_jacket(ebook): - report(_('Existing metadata jacket replaced')) - else: - report(_('Metadata jacket inserted')) - else: - report(_('Existing metadata jacket replaced')) - report('') - - if opts.remove_jacket: - rt(_('Removing metadata jacket')) - if remove_jacket(ebook): - report(_('Metadata jacket removed')) - else: - report(_('No metadata jacket found')) - report('') - - if opts.smarten_punctuation: - rt(_('Smartening punctuation')) - smarten_punctuation(ebook, report) - report('') - - if opts.embed: - rt(_('Embedding referenced fonts')) - embed_all_fonts(ebook, stats, report) - report('') - - if opts.subset: - rt(_('Subsetting embedded fonts')) - subset_all_fonts(ebook, stats.font_stats, report) - report('') - + polish_one(ebook, opts, report) ebook.commit(outbook) report('-'*70) report(_('Polishing took: %.1f seconds')%(time.time()-st)) @@ -204,6 +207,15 @@ def gui_polish(data): log(msg) return '\n\n'.join(report) +def tweak_polish(container, actions): + opts = ALL_OPTS.copy() + opts.update(actions) + O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys())) + opts = O(**opts) + report = [] + polish_one(container, opts, report.append) + return report + def option_parser(): from calibre.utils.config import OptionParser USAGE = '%prog [options] input_file [output_file]\n\n' + re.sub( diff --git a/src/calibre/ebooks/oeb/polish/replace.py b/src/calibre/ebooks/oeb/polish/replace.py index a78f7b5ef7..7945787dd3 100644 --- a/src/calibre/ebooks/oeb/polish/replace.py +++ b/src/calibre/ebooks/oeb/polish/replace.py @@ -78,6 +78,7 @@ def replace_links(container, link_map, frag_map=lambda name, frag:frag, replace_ def smarten_punctuation(container, report): from calibre.ebooks.conversion.preprocess import smarten_punctuation + smartened = False for path in container.spine_items: name = container.abspath_to_name(path) changed = False @@ -98,6 +99,9 @@ def smarten_punctuation(container, report): for m in root.xpath('descendant::*[local-name()="meta" and @http-equiv]'): m.getparent().remove(m) container.dirty(name) + smartened = True + if not smartened: + report(_('No punctuation that could be smartened found')) def rename_files(container, file_map): overlap = set(file_map).intersection(set(file_map.itervalues())) diff --git a/src/calibre/ebooks/unihandecode/unicodepoints.py b/src/calibre/ebooks/unihandecode/unicodepoints.py index 05cd51cadb..fe1495de73 100644 --- a/src/calibre/ebooks/unihandecode/unicodepoints.py +++ b/src/calibre/ebooks/unihandecode/unicodepoints.py @@ -178,7 +178,7 @@ CODEPOINTS = { 'x06': [ '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', ',', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', ';', '[?]', '[?]', '[?]', '?', - '[?]', '', 'a', '\'', 'w\'', '', 'y\'', '', 'b', '@', 't', 'th', 'j', 'H', 'kh', 'd', + '[?]', '', 'a', '\'', 'w\'', '', 'y\'', 'a', 'b', '@', 't', 'th', 'j', 'H', 'kh', 'd', 'dh', 'r', 'z', 's', 'sh', 'S', 'D', 'T', 'Z', '`', 'G', '[?]', '[?]', '[?]', '[?]', '[?]', '', 'f', 'q', 'k', 'l', 'm', 'n', 'h', 'w', '~', 'y', 'an', 'un', 'in', 'a', 'u', 'i', 'W', '', '', '\'', '\'', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 67d4b80750..01a4310c82 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -876,6 +876,7 @@ class Application(QApplication): 'MessageBoxWarning': u'dialog_warning.png', 'MessageBoxCritical': u'dialog_error.png', 'MessageBoxQuestion': u'dialog_question.png', + 'BrowserReload': u'view-refresh.png', # These two are used to calculate the sizes for the doc widget # title bar buttons, therefore, they have to exist. The actual # icon is not used. diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index aff0f23a5d..dddcc1ca4e 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -24,5 +24,12 @@ def set_current_container(container): global _current_container _current_container = container -actions = {} -editors = {} +class NonReplaceDict(dict): + + def __setitem__(self, k, v): + if k in self: + raise ValueError('The key %s is already present' % k) + dict.__setitem__(self, k, v) + +actions = NonReplaceDict() +editors = NonReplaceDict() diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 7269f3934e..556dd90c7f 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -11,12 +11,12 @@ from functools import partial from PyQt4.Qt import ( QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, - QDialogButtonBox, QIcon, QTimer, QPixmap) + QDialogButtonBox, QIcon, QTimer, QPixmap, QTextBrowser, QVBoxLayout) from calibre import prints from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ebooks.oeb.base import urlnormalize -from calibre.ebooks.oeb.polish.main import SUPPORTED +from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type from calibre.ebooks.oeb.polish.replace import rename_files from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog @@ -25,6 +25,7 @@ from calibre.gui2.tweak_book import set_current_container, current_container, tp from calibre.gui2.tweak_book.undo import GlobalUndoHistory from calibre.gui2.tweak_book.save import SaveManager from calibre.gui2.tweak_book.preview import parse_worker +from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime def get_container(*args, **kwargs): @@ -86,6 +87,9 @@ class Boss(QObject): ' Convert your book to one of these formats first.') % _(' and ').join(sorted(SUPPORTED)), show=True) + for name in tuple(editors): + self.close_editor(name) + self.gui.preview.clear() self.container_count = -1 if self.tdir: shutil.rmtree(self.tdir, ignore_errors=True) @@ -97,6 +101,7 @@ class Boss(QObject): return error_dialog(self.gui, _('Failed to open book'), _('Failed to open book, click Show details for more information.'), det_msg=job.traceback, show=True) + parse_worker.clear() container = job.result set_current_container(container) self.current_metadata = self.gui.current_metadata = container.mi @@ -106,12 +111,20 @@ class Boss(QObject): self.gui.action_save.setEnabled(False) self.update_global_history_actions() + def update_editors_from_container(self, container=None): + c = container or current_container() + for name, ed in tuple(editors.iteritems()): + if c.has_name(name): + ed.replace_data(c.raw_data(name)) + else: + self.close_editor(name) + def apply_container_update_to_gui(self): container = current_container() self.gui.file_list.build(container) self.update_global_history_actions() self.gui.action_save.setEnabled(True) - # TODO: Apply to other GUI elements + self.update_editors_from_container() def delete_requested(self, spine_items, other_items): if not self.check_dirtied(): @@ -126,7 +139,8 @@ class Boss(QObject): for name in list(spine_items) + list(other_items): if name in editors: self.close_editor(name) - # TODO: Update other GUI elements + if not editors: + self.gui.preview.clear() def reorder_spine(self, items): # TODO: If content.opf is dirty in an editor, abort, calling @@ -138,6 +152,41 @@ class Boss(QObject): self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items # TODO: If content.opf is open in an editor, reload it + def edit_toc(self): + if not self.check_dirtied(): + return + self.add_savepoint(_('Edit Table of Contents')) + d = TOCEditor(title=self.current_metadata.title, parent=self.gui) + if d.exec_() != d.Accepted: + self.rewind_savepoint() + return + self.update_editors_from_container() + + def polish(self, action, name): + if not self.check_dirtied(): + return + self.add_savepoint(name) + try: + report = tweak_polish(current_container(), {action:True}) + except: + self.rewind_savepoint() + raise + self.apply_container_update_to_gui() + from calibre.ebooks.markdown import markdown + report = markdown('# %s\n\n'%self.current_metadata.title + '\n\n'.join(report), output_format='html4') + d = QDialog(self.gui) + d.l = QVBoxLayout() + d.setLayout(d.l) + d.e = QTextBrowser(d) + d.l.addWidget(d.e) + d.e.setHtml(report) + d.bb = QDialogButtonBox(QDialogButtonBox.Close) + d.l.addWidget(d.bb) + d.bb.rejected.connect(d.reject) + d.bb.accepted.connect(d.accept) + d.resize(600, 400) + d.exec_() + # Renaming {{{ def rename_requested(self, oldname, newname): if not self.check_dirtied(): @@ -172,7 +221,8 @@ class Boss(QObject): det_msg=job.traceback, show=True) self.gui.file_list.build(current_container()) self.gui.action_save.setEnabled(True) - # TODO: Update the rest of the GUI + # TODO: Update the rest of the GUI. This means renaming open editors and + # then calling update_editors_from_container() # }}} # Global history {{{ @@ -234,6 +284,7 @@ class Boss(QObject): editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs) editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed) editor.data_changed.connect(self.editor_data_changed) + editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed) c = current_container() with c.open(name) as f: editor.data = c.decode(f.read()) @@ -252,6 +303,7 @@ class Boss(QObject): _('Editing files of type %s is not supported' % mime), show=True) self.edit_file(name, syntax) + # Editor basic controls {{{ def do_editor_undo(self): ed = self.gui.central.current_editor if ed is not None: @@ -262,16 +314,35 @@ class Boss(QObject): if ed is not None: ed.redo() + def do_editor_copy(self): + ed = self.gui.central.current_editor + if ed is not None: + ed.copy() + + def do_editor_cut(self): + ed = self.gui.central.current_editor + if ed is not None: + ed.cut() + + def do_editor_paste(self): + ed = self.gui.central.current_editor + if ed is not None: + ed.paste() + def editor_data_changed(self, editor): - self.gui.preview.refresh_timer.start(tprefs['preview_refresh_time'] * 1000) + self.gui.preview.start_refresh_timer() def editor_undo_redo_state_changed(self, *args): self.apply_current_editor_state(update_keymap=False) + def editor_copy_available_state_changed(self, *args): + self.apply_current_editor_state(update_keymap=False) + def editor_modification_state_changed(self, is_modified): self.apply_current_editor_state(update_keymap=False) if is_modified: actions['save-book'].setEnabled(True) + # }}} def apply_current_editor_state(self, update_keymap=True): ed = self.gui.central.current_editor @@ -279,6 +350,8 @@ class Boss(QObject): actions['editor-undo'].setEnabled(ed.undo_available) actions['editor-redo'].setEnabled(ed.redo_available) actions['editor-save'].setEnabled(ed.is_modified) + actions['editor-cut'].setEnabled(ed.copy_available) + actions['editor-copy'].setEnabled(ed.cut_available) self.gui.keyboard.set_mode(ed.syntax) name = None for n, x in editors.iteritems(): @@ -308,6 +381,8 @@ class Boss(QObject): editor = editors.pop(name) self.gui.central.close_editor(editor) editor.break_cycles() + if not editors: + self.gui.preview.clear() def do_editor_save(self): ed = self.gui.central.current_editor @@ -397,7 +472,7 @@ class Boss(QObject): QApplication.instance().quit() def shutdown(self): - self.gui.preview.refresh_timer.stop() + self.gui.preview.stop_refresh_timer() self.save_state() self.save_manager.shutdown() parse_worker.shutdown() diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 91efa90f8e..afd3e8c6dc 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -14,7 +14,7 @@ from PyQt4.Qt import ( QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect) from calibre.gui2.tweak_book import tprefs -from calibre.gui2.tweak_book.editor.themes import THEMES, DEFAULT_THEME, theme_color +from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter from calibre.gui2.tweak_book.editor.syntax.css import CSSHighlighter @@ -74,7 +74,7 @@ class TextEdit(QPlainTextEdit): self.setLineWrapMode(QPlainTextEdit.WidgetWidth if prefs['editor_line_wrap'] else QPlainTextEdit.NoWrap) theme = THEMES.get(prefs['editor_theme'], None) if theme is None: - theme = THEMES[DEFAULT_THEME] + theme = THEMES[default_theme()] self.apply_theme(theme) def apply_theme(self, theme): @@ -114,6 +114,18 @@ class TextEdit(QPlainTextEdit): self.highlighter.setDocument(self.document()) self.setPlainText(text) + def replace_text(self, text): + c = self.textCursor() + pos = c.position() + c.beginEditBlock() + c.clearSelection() + c.select(c.Document) + c.insertText(text) + c.endEditBlock() + c.setPosition(min(pos, len(text))) + self.setTextCursor(c) + self.ensureCursorVisible() + # Line numbers and cursor line {{{ def highlight_cursor_line(self): sel = QTextEdit.ExtraSelection() diff --git a/src/calibre/gui2/tweak_book/editor/themes.py b/src/calibre/gui2/tweak_book/editor/themes.py index 3d3b5fa618..fcd35fdad5 100644 --- a/src/calibre/gui2/tweak_book/editor/themes.py +++ b/src/calibre/gui2/tweak_book/editor/themes.py @@ -8,21 +8,67 @@ __copyright__ = '2013, Kovid Goyal ' from collections import namedtuple -from PyQt4.Qt import (QColor, QTextCharFormat, QBrush, QFont) +from PyQt4.Qt import (QColor, QTextCharFormat, QBrush, QFont, QApplication, QPalette) underline_styles = {'single', 'dash', 'dot', 'dash_dot', 'dash_dot_dot', 'wave', 'spell'} -DEFAULT_THEME = 'calibre-dark' +_default_theme = None +def default_theme(): + global _default_theme + if _default_theme is None: + isdark = QApplication.instance().palette().color(QPalette.WindowText).lightness() > 128 + _default_theme = 'wombat-dark' if isdark else 'pyte-light' + return _default_theme + +# The solarized themes {{{ +SLDX = {'base03':'1c1c1c', 'base02':'262626', 'base01':'585858', 'base00':'626262', 'base0':'808080', 'base1':'8a8a8a', 'base2':'e4e4e4', 'base3':'ffffd7', 'yellow':'af8700', 'orange':'d75f00', 'red':'d70000', 'magenta':'af005f', 'violet':'5f5faf', 'blue':'0087ff', 'cyan':'00afaf', 'green':'5f8700'} # noqa +SLD = {'base03':'002b36', 'base02':'073642', 'base01':'586e75', 'base00':'657b83', 'base0':'839496', 'base1':'93a1a1', 'base2':'eee8d5', 'base3':'fdf6e3', 'yellow':'b58900', 'orange':'cb4b16', 'red':'dc322f', 'magenta':'d33682', 'violet':'6c71c4', 'blue':'268bd2', 'cyan':'2aa198', 'green':'859900'} # noqa +m = {'base%d'%n:'base%02d'%n for n in xrange(1, 4)} +m.update({'base%02d'%n:'base%d'%n for n in xrange(1, 4)}) +SLL = {m.get(k, k) : v for k, v in SLD.iteritems()} +SLLX = {m.get(k, k) : v for k, v in SLDX.iteritems()} +SOLARIZED = \ + ''' + CursorLine bg={base02} + CursorColumn bg={base02} + ColorColumn bg={base02} + MatchParen fg={red} bg={base01} bold + Pmenu fg={base0} bg={base02} + PmenuSel fg={base01} bg={base2} + + Cursor fg={base03} bg={base0} + Normal fg={base0} bg={base03} + LineNr fg={base01} bg={base02} + LineNrC fg={magenta} + Visual fg={base01} bg={base03} + + Comment fg={base01} italic + Todo fg={magenta} bold + String fg={cyan} + Constant fg={cyan} + Number fg={cyan} + PreProc fg={orange} + Identifier fg={blue} + Function fg={blue} + Type fg={yellow} + Statement fg={green} bold + Keyword fg={green} + Special fg={red} + + Error us=wave uc={red} + Tooltip fg=black bg=ffffed + ''' +# }}} THEMES = { - 'calibre-dark': # {{{ Based on the wombat color scheme for vim + 'wombat-dark': # {{{ ''' - CursorLine bg=2d2d2d - CursorColumn bg=2d2d2d - ColorColumn bg=2d2d2d + CursorLine bg={cursor_loc} + CursorColumn bg={cursor_loc} + ColorColumn bg={cursor_loc} MatchParen fg=f6f3e8 bg=857b6f bold Pmenu fg=f6f3e8 bg=444444 - PmenuSel fg=yellow bg=cae682 + PmenuSel fg=yellow bg={identifier} Tooltip fg=black bg=ffffed Cursor bg=656565 @@ -31,27 +77,79 @@ THEMES = { LineNrC fg=yellow Visual fg=f6f3e8 bg=444444 - Comment fg=99968b + Comment fg={comment} Todo fg=8f8f8f - String fg=95e454 - Identifier fg=cae682 - Function fg=cae682 - Type fg=cae682 - Statement fg=8ac6f2 - Keyword fg=8ac6f2 - Constant fg=e5786d - PreProc fg=e5786d - Number fg=e5786d + String fg={string} + Constant fg={constant} + Number fg={constant} + PreProc fg={constant} + Identifier fg={identifier} + Function fg={identifier} + Type fg={identifier} + Statement fg={keyword} + Keyword fg={keyword} Special fg=e7f6da Error us=wave uc=red - ''', # }}} + '''.format( + cursor_loc='2d2d2d', + identifier='cae682', + comment='99968b', + string='95e454', + keyword='8ac6f2', + constant='e5786d'), # }}} + + 'pyte-light': # {{{ + ''' + CursorLine bg={cursor_loc} + CursorColumn bg={cursor_loc} + ColorColumn bg={cursor_loc} + MatchParen fg=white bg=80a090 bold + Pmenu fg=white bg=808080 + PmenuSel fg=white bg=808080 + Tooltip fg=black bg=ffffed + + Cursor fg=black bg=b0b4b8 + Normal fg=404850 bg=f0f0f0 + LineNr fg=white bg=8090a0 + LineNrC fg=yellow + Visual fg=white bg=8090a0 + + Comment fg={comment} italic + Todo fg={comment} italic bold + String fg={string} + Constant fg={constant} + Number fg={constant} + PreProc fg={constant} + Identifier fg={identifier} + Function fg={identifier} + Type fg={identifier} + Statement fg={keyword} + Keyword fg={keyword} + Special fg=70a0d0 italic + Error us=wave uc=red + + '''.format( + cursor_loc='white', + identifier='7b5694', + comment='a0b0c0', + string='4070a0', + keyword='007020', + constant='a07040'), # }}} + + 'solarized-x-dark': SOLARIZED.format(**SLDX), + 'solarized-dark': SOLARIZED.format(**SLD), + 'solarized-light': SOLARIZED.format(**SLL), + 'solarized-x-light': SOLARIZED.format(**SLLX), } def read_color(col): if QColor.isValidColor(col): return QBrush(QColor(col)) + if col.startswith('rgb('): + r, g, b = map(int, (x.strip() for x in col[4:-1].split(','))) + return QBrush(QColor(r, g, b)) try: r, g, b = col[0:2], col[2:4], col[4:6] r, g, b = int(r, 16), int(g, 16), int(b, 16) @@ -122,5 +220,5 @@ def theme_color(theme, name, attr): try: return getattr(theme[name], attr).color() except (KeyError, AttributeError): - return getattr(THEMES[DEFAULT_THEME], attr).color() + return getattr(THEMES[default_theme()], attr).color() diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index 13393d72d8..088794b4f0 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -9,6 +9,7 @@ __copyright__ = '2013, Kovid Goyal ' from PyQt4.Qt import QMainWindow, Qt, QApplication, pyqtSignal from calibre import xml_replace_entities +from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import actions from calibre.gui2.tweak_book.editor.text import TextEdit @@ -16,6 +17,7 @@ class Editor(QMainWindow): modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) + copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) def __init__(self, syntax, parent=None): @@ -29,20 +31,11 @@ class Editor(QMainWindow): self.create_toolbars() self.undo_available = False self.redo_available = False + self.copy_available = self.cut_available = False self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) - - def _data_changed(self): - self.data_changed.emit(self) - - def _undo_available(self, available): - self.undo_available = available - self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) - - def _redo_available(self, available): - self.redo_available = available - self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) + self.editor.copyAvailable.connect(self._copy_available) @dynamic_property def data(self): @@ -58,6 +51,13 @@ class Editor(QMainWindow): def get_raw_data(self): return unicode(self.editor.toPlainText()) + def replace_data(self, raw, only_if_different=True): + if isinstance(raw, bytes): + raw = raw.decode('utf-8') + current = self.get_raw_data() if only_if_different else False + if current != raw: + self.editor.replace_text(raw) + def undo(self): self.editor.undo() @@ -73,22 +73,62 @@ class Editor(QMainWindow): return property(fget=fget, fset=fset) def create_toolbars(self): - self.action_bar = b = self.addToolBar(_('Edit actions tool bar')) + self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState - b.addAction(actions['editor-save']) - b.addAction(actions['editor-undo']) - b.addAction(actions['editor-redo']) + for x in ('save', 'undo', 'redo'): + try: + b.addAction(actions['editor-%s' % x]) + except KeyError: + pass + self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) + for x in ('cut', 'copy', 'paste'): + try: + b.addAction(actions['editor-%s' % x]) + except KeyError: + pass def break_cycles(self): self.modification_state_changed.disconnect() self.undo_redo_state_changed.disconnect() + self.copy_available_state_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() + self.editor.copyAvailable.disconnect() self.editor.setPlainText('') + def _data_changed(self): + self.data_changed.emit(self) + + def _undo_available(self, available): + self.undo_available = available + self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) + + def _redo_available(self, available): + self.redo_available = available + self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) + + def _copy_available(self, available): + self.copy_available = self.cut_available = available + self.copy_available_state_changed.emit(available) + + def cut(self): + self.editor.cut() + + def copy(self): + self.editor.copy() + + def paste(self): + if not self.editor.canPaste(): + return error_dialog(self, _('No text'), _( + 'There is no suitable text in the clipboard to paste.'), show=True) + self.editor.paste() + + def contextMenuEvent(self, ev): + ev.ignore() + def launch_editor(path_to_edit, path_is_raw=False, syntax='html'): if path_is_raw: raw = path_to_edit @@ -102,7 +142,7 @@ def launch_editor(path_to_edit, path_is_raw=False, syntax='html'): syntax = 'css' app = QApplication([]) t = Editor(syntax) - t.load_text(raw, syntax=syntax) + t.data = raw t.show() app.exec_() diff --git a/src/calibre/gui2/tweak_book/preview.py b/src/calibre/gui2/tweak_book/preview.py index 59efe2fc70..eabf1e22c9 100644 --- a/src/calibre/gui2/tweak_book/preview.py +++ b/src/calibre/gui2/tweak_book/preview.py @@ -11,16 +11,16 @@ from threading import Thread from Queue import Queue, Empty from PyQt4.Qt import ( - QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager, - QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QNetworkDiskCache) -from PyQt4.QtWebKit import QWebView + QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager, QMenu, QIcon, + QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QNetworkDiskCache, QToolBar) +from PyQt4.QtWebKit import QWebView, QWebInspector from calibre import prints from calibre.constants import iswindows from calibre.ebooks.oeb.polish.parsing import parse from calibre.ebooks.oeb.base import serialize, OEB_DOCS from calibre.ptempfile import PersistentTemporaryDirectory -from calibre.gui2.tweak_book import current_container, editors +from calibre.gui2.tweak_book import current_container, editors, tprefs, actions from calibre.gui2.viewer.documentview import apply_settings from calibre.gui2.viewer.config import config from calibre.utils.ipc.simple_worker import offload_worker @@ -117,6 +117,9 @@ class ParseWorker(Thread): def get_data(self, name): return getattr(self.parse_items.get(name, None), 'parsed_data', None) + def clear(self): + self.parse_items.clear() + parse_worker = ParseWorker() # }}} @@ -218,6 +221,7 @@ class WebView(QWebView): def __init__(self, parent=None): QWebView.__init__(self, parent) + self.inspector = QWebInspector(self) w = QApplication.instance().desktop().availableGeometry(self).width() self._size_hint = QSize(int(w/3), int(w/2)) settings = self.page().settings() @@ -232,10 +236,11 @@ class WebView(QWebView): settings.setAttribute(settings.DeveloperExtrasEnabled, True) settings.setDefaultTextEncoding('utf-8') - self.setHtml('

') self.page().setNetworkAccessManager(NetworkAccessManager(self)) self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) + self.clear() + def sizeHint(self): return self._size_hint @@ -253,6 +258,20 @@ class WebView(QWebView): mf.setScrollBarValue(Qt.Vertical, val[1]) return property(fget=fget, fset=fset) + def clear(self): + self.setHtml('

') + + def inspect(self): + self.inspector.parent().show() + self.inspector.parent().raise_() + self.pageAction(self.page().InspectElement).trigger() + + def contextMenuEvent(self, ev): + menu = QMenu(self) + menu.addAction(actions['reload-preview']) + menu.addAction(QIcon(I('debug.png')), _('Inspect element'), self.inspect) + menu.exec_(ev.globalPos()) + class Preview(QWidget): def __init__(self, parent=None): @@ -261,7 +280,24 @@ class Preview(QWidget): self.setLayout(l) l.setContentsMargins(0, 0, 0, 0) self.view = WebView(self) + self.inspector = self.view.inspector + self.inspector.setPage(self.view.page()) l.addWidget(self.view) + self.bar = QToolBar(self) + l.addWidget(self.bar) + + ac = actions['auto-reload-preview'] + ac.setCheckable(True) + ac.setChecked(True) + ac.toggled.connect(self.auto_reload_toggled) + self.auto_reload_toggled(ac.isChecked()) + self.bar.addAction(ac) + + ac = actions['reload-preview'] + ac.triggered.connect(self.refresh) + self.bar.addAction(ac) + + actions['preview-dock'].toggled.connect(self.visibility_changed) self.current_name = None self.last_sync_request = None @@ -278,9 +314,38 @@ class Preview(QWidget): def refresh(self): if self.current_name: + self.refresh_timer.stop() # This will check if the current html has changed in its editor, # and re-parse it if so parse_worker.add_request(self.current_name) # Tell webkit to reload all html and associated resources - self.view.refresh() + current_url = QUrl.fromLocalFile(current_container().name_to_abspath(self.current_name)) + if current_url != self.view.url(): + # The container was changed + self.view.setUrl(current_url) + else: + self.view.refresh() + + def clear(self): + self.view.clear() + + @property + def is_visible(self): + return actions['preview-dock'].isChecked() + + def start_refresh_timer(self): + if self.is_visible and actions['auto-reload-preview'].isChecked(): + self.refresh_timer.start(tprefs['preview_refresh_time'] * 1000) + + def stop_refresh_timer(self): + self.refresh_timer.stop() + + def auto_reload_toggled(self, checked): + actions['auto-reload-preview'].setToolTip(_( + 'Auto reload preview when text changes in editor') if not checked else _( + 'Disable auto reload of preview')) + + def visibility_changed(self, is_visible): + if is_visible: + self.refresh() diff --git a/src/calibre/gui2/tweak_book/toc.py b/src/calibre/gui2/tweak_book/toc.py new file mode 100644 index 0000000000..fa828a4f0f --- /dev/null +++ b/src/calibre/gui2/tweak_book/toc.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +from PyQt4.Qt import (QDialog, pyqtSignal, QIcon, QVBoxLayout, QDialogButtonBox, QStackedWidget) + +from calibre.ebooks.oeb.polish.toc import commit_toc +from calibre.gui2 import gprefs, error_dialog +from calibre.gui2.toc.main import TOCView, ItemEdit +from calibre.gui2.tweak_book import current_container + +class TOCEditor(QDialog): + + explode_done = pyqtSignal(object) + writing_done = pyqtSignal(object) + + def __init__(self, title=None, parent=None): + QDialog.__init__(self, parent) + + t = title or current_container().mi.title + self.book_title = t + self.setWindowTitle(_('Edit the ToC in %s')%t) + self.setWindowIcon(QIcon(I('toc.png'))) + + l = self.l = QVBoxLayout() + self.setLayout(l) + + self.stacks = s = QStackedWidget(self) + l.addWidget(s) + self.toc_view = TOCView(self) + self.toc_view.add_new_item.connect(self.add_new_item) + s.addWidget(self.toc_view) + self.item_edit = ItemEdit(self) + s.addWidget(self.item_edit) + + bb = self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + l.addWidget(bb) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + + self.read_toc() + + self.resize(950, 630) + geom = gprefs.get('toc_editor_window_geom', None) + if geom is not None: + self.restoreGeometry(bytes(geom)) + + def add_new_item(self, item, where): + self.item_edit(item, where) + self.stacks.setCurrentIndex(1) + + def accept(self): + if self.stacks.currentIndex() == 1: + self.toc_view.update_item(*self.item_edit.result) + gprefs['toc_edit_splitter_state'] = bytearray(self.item_edit.splitter.saveState()) + self.stacks.setCurrentIndex(0) + elif self.stacks.currentIndex() == 0: + self.write_toc() + super(TOCEditor, self).accept() + + def really_accept(self, tb): + gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry()) + if tb: + error_dialog(self, _('Failed to write book'), + _('Could not write %s. Click "Show details" for' + ' more information.')%self.book_title, det_msg=tb, show=True) + gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry()) + super(TOCEditor, self).reject() + return + + super(TOCEditor, self).accept() + + def reject(self): + if not self.bb.isEnabled(): + return + if self.stacks.currentIndex() == 1: + gprefs['toc_edit_splitter_state'] = bytearray(self.item_edit.splitter.saveState()) + self.stacks.setCurrentIndex(0) + else: + gprefs['toc_editor_window_geom'] = bytearray(self.saveGeometry()) + super(TOCEditor, self).reject() + + def read_toc(self): + self.toc_view(current_container()) + self.item_edit.load(current_container()) + self.stacks.setCurrentIndex(0) + + def write_toc(self): + toc = self.toc_view.create_toc() + commit_toc(current_container(), toc, lang=self.toc_view.toc_lang, + uid=self.toc_view.toc_uid) + diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 46d223e61a..d69fbab793 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -6,6 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' +from functools import partial + from PyQt4.Qt import ( QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget, QVBoxLayout, QStackedWidget, QTabWidget, QImage, QPixmap, pyqtSignal) @@ -20,6 +22,7 @@ from calibre.gui2.tweak_book.keyboard import KeyboardManager from calibre.gui2.tweak_book.preview import Preview class Central(QStackedWidget): + ' The central widget, hosts the editors ' current_editor_changed = pyqtSignal() @@ -106,9 +109,9 @@ class Main(MainWindow): self.keyboard = KeyboardManager() self.create_actions() - self.create_menubar() - self.create_toolbar() + self.create_toolbars() self.create_docks() + self.create_menubar() self.status_bar = self.statusBar() self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget) @@ -138,7 +141,8 @@ class Main(MainWindow): def reg(icon, text, target, sid, keys, description): ac = actions[sid] = QAction(QIcon(I(icon)), text, self) ac.setObjectName('action-' + sid) - ac.triggered.connect(target) + if target is not None: + ac.triggered.connect(target) if isinstance(keys, type('')): keys = (keys,) self.keyboard.register_shortcut( @@ -156,12 +160,44 @@ class Main(MainWindow): self.action_quit = reg('quit.png', _('&Quit'), self.boss.quit, 'quit', 'Ctrl+Q', _('Quit')) # Editor actions + group = _('Editor actions') self.action_editor_undo = reg('edit-undo.png', _('&Undo'), self.boss.do_editor_undo, 'editor-undo', 'Ctrl+Z', _('Undo typing')) self.action_editor_redo = reg('edit-redo.png', _('&Redo'), self.boss.do_editor_redo, 'editor-redo', 'Ctrl+Y', _('Redo typing')) self.action_editor_save = reg('save.png', _('&Save'), self.boss.do_editor_save, 'editor-save', 'Ctrl+S', _('Save changes to the current file')) + self.action_editor_cut = reg('edit-cut.png', _('C&ut text'), self.boss.do_editor_cut, 'editor-cut', ('Ctrl+X', 'Shift+Delete', ), + _('Cut text')) + self.action_editor_copy = reg('edit-copy.png', _('&Copy text'), self.boss.do_editor_copy, 'editor-copy', ('Ctrl+C', 'Ctrl+Insert'), + _('Copy text')) + self.action_editor_paste = reg('edit-paste.png', _('&Paste text'), self.boss.do_editor_paste, 'editor-paste', ('Ctrl+V', 'Shift+Insert', ), + _('Paste text')) + self.action_editor_cut.setEnabled(False) + self.action_editor_copy.setEnabled(False) + self.action_editor_undo.setEnabled(False) + self.action_editor_redo.setEnabled(False) + + # Tool actions + group = _('Tools') + self.action_toc = reg('toc.png', _('&Edit Table of Contents'), self.boss.edit_toc, 'edit-toc', (), _('Edit Table of Contents')) + + # Polish actions + group = _('Polish') + self.action_subset_fonts = reg( + 'subset-fonts.png', _('&Subset embedded fonts'), partial( + self.boss.polish, 'subset', _('Subset fonts')), 'subset-fonts', (), _('Subset embedded fonts')) + self.action_embed_fonts = reg( + 'embed-fonts.png', _('&Embed referenced fonts'), partial( + self.boss.polish, 'embed', _('Embed fonts')), 'embed-fonts', (), _('Embed referenced fonts')) + self.action_smarten_punctuation = reg( + 'smarten-punctuation.png', _('&Smarten punctuation'), partial( + self.boss.polish, 'smarten_punctuation', _('Smarten punctuation')), 'smarten-punctuation', (), _('Smarten punctuation')) + + # Preview actions + group = _('Preview') + self.action_auto_reload_preview = reg('auto-reload.png', _('Auto reload preview'), None, 'auto-reload-preview', (), _('Auto reload preview')) + self.action_reload_preview = reg('view-refresh.png', _('Refresh preview'), None, 'reload-preview', ('F5', 'Ctrl+R'), _('Refresh preview')) def create_menubar(self): b = self.menuBar() @@ -174,30 +210,78 @@ class Main(MainWindow): e = b.addMenu(_('&Edit')) e.addAction(self.action_global_undo) e.addAction(self.action_global_redo) + e.addSeparator() + e.addAction(self.action_editor_undo) + e.addAction(self.action_editor_redo) + e.addSeparator() + e.addAction(self.action_editor_cut) + e.addAction(self.action_editor_copy) + e.addAction(self.action_editor_paste) - def create_toolbar(self): - self.global_bar = b = self.addToolBar(_('Global tool bar')) - b.setObjectName('global_bar') # Needed for saveState - b.addAction(self.action_open_book) - b.addAction(self.action_global_undo) - b.addAction(self.action_global_redo) - b.addAction(self.action_save) + e = b.addMenu(_('&Tools')) + e.addAction(self.action_toc) + e.addAction(self.action_embed_fonts) + e.addAction(self.action_subset_fonts) + e.addAction(self.action_smarten_punctuation) + + e = b.addMenu(_('&View')) + t = e.addMenu(_('Tool&bars')) + e.addSeparator() + for name, ac in actions.iteritems(): + if name.endswith('-dock'): + e.addAction(ac) + elif name.endswith('-bar'): + t.addAction(ac) + + def create_toolbars(self): + def create(text, name): + name += '-bar' + b = self.addToolBar(text) + b.setObjectName(name) # Needed for saveState + setattr(self, name.replace('-', '_'), b) + actions[name] = b.toggleViewAction() + return b + + a = create(_('Book tool bar'), 'global').addAction + for x in ('open_book', 'global_undo', 'global_redo', 'save', 'toc'): + a(getattr(self, 'action_' + x)) + + a = create(_('Polish book tool bar'), 'polish').addAction + for x in ('embed_fonts', 'subset_fonts', 'smarten_punctuation'): + a(getattr(self, 'action_' + x)) def create_docks(self): - self.file_list_dock = d = QDockWidget(_('&Files Browser'), self) - d.setObjectName('file_list_dock') # Needed for saveState + + def create(name, oname): + oname += '-dock' + d = QDockWidget(name, self) + d.setObjectName(oname) # Needed for saveState + ac = d.toggleViewAction() + desc = _('Toggle %s') % name.replace('&', '') + self.keyboard.register_shortcut( + oname, desc, description=desc, action=ac, group=_('Windows')) + actions[oname] = ac + setattr(self, oname.replace('-', '_'), d) + return d + + d = create(_('&Files Browser'), 'files-browser') d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.file_list = FileListWidget(d) d.setWidget(self.file_list) self.addDockWidget(Qt.LeftDockWidgetArea, d) - self.preview_dock = d = QDockWidget(_('&Book preview'), self) - d.setObjectName('file_list_dock') # Needed for saveState + d = create(_('File &Preview'), 'preview') d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.preview = Preview(d) d.setWidget(self.preview) self.addDockWidget(Qt.RightDockWidgetArea, d) + d = create(_('&Inspector'), 'inspector') + d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea) + d.setWidget(self.preview.inspector) + self.preview.inspector.setParent(d) + self.addDockWidget(Qt.BottomDockWidgetArea, d) + def resizeEvent(self, ev): self.blocking_job.resize(ev.size()) return super(Main, self).resizeEvent(ev) @@ -227,3 +311,8 @@ class Main(MainWindow): state = tprefs.get('main_window_state', None) if state is not None: self.restoreState(state, self.STATE_VERSION) + # We never want to start with the inspector showing + self.inspector_dock.close() + + def contextMenuEvent(self, ev): + ev.ignore() diff --git a/src/calibre/gui2/tweak_book/undo.py b/src/calibre/gui2/tweak_book/undo.py index f9bd9ba810..ae388c56ce 100644 --- a/src/calibre/gui2/tweak_book/undo.py +++ b/src/calibre/gui2/tweak_book/undo.py @@ -57,7 +57,7 @@ class GlobalUndoHistory(object): revert to state before creating savepoint. ''' if self.pos > 0 and self.pos == len(self.states) - 1: self.pos -= 1 - cleanup(self.states.pop()) + cleanup([self.states.pop().container]) ans = self.current_container ans.message = None return ans