Merge branch 'kovidgoyal/master'
@ -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
|
||||
|
||||
|
1566
imgsrc/view-refresh.svg
Normal file
After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.7 KiB |
96
recipes/argnoticias.recipe
Normal file
@ -0,0 +1,96 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Darko Miletic <darko.miletic at gmail.com>'
|
||||
|
||||
'''
|
||||
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
|
BIN
recipes/icons/argnoticias.png
Normal file
After Width: | Height: | Size: 171 B |
@ -6,7 +6,6 @@ __copyright__ = '''2010, matek09, matek09@gmail.com
|
||||
Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
import re
|
||||
|
||||
class Wprost(BasicNewsRecipe):
|
||||
title = u'Wprost (RSS)'
|
||||
|
BIN
resources/images/auto-reload.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
resources/images/embed-fonts.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
resources/images/smarten-punctuation.png
Normal file
After Width: | Height: | Size: 776 B |
BIN
resources/images/subset-fonts.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
resources/images/view-refresh.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
@ -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 <kovid@kovidgoyal.net>"
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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()))
|
||||
|
@ -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', '', '', '\'', '\'', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]',
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -8,21 +8,67 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
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()
|
||||
|
||||
|
@ -9,6 +9,7 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
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_()
|
||||
|
||||
|
@ -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('<p>')
|
||||
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('<p>')
|
||||
|
||||
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()
|
||||
|
||||
|
96
src/calibre/gui2/tweak_book/toc.py
Normal file
@ -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 <kovid at kovidgoyal.net>'
|
||||
|
||||
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)
|
||||
|
@ -6,6 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
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()
|
||||
|
@ -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
|
||||
|