Merge branch 'kovidgoyal/master'

This commit is contained in:
Charles Haley 2013-11-09 13:02:59 +01:00
commit 60a6212835
26 changed files with 2322 additions and 124 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

View File

@ -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)'

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -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>"

View File

@ -119,12 +119,8 @@ 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)
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:
@ -178,6 +174,13 @@ def polish(file_map, opts, log, report):
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)
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(

View File

@ -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()))

View File

@ -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', '', '', '\'', '\'', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]', '[?]',

View File

@ -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.

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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_()

View File

@ -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
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()

View 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)

View File

@ -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,6 +141,7 @@ class Main(MainWindow):
def reg(icon, text, target, sid, keys, description):
ac = actions[sid] = QAction(QIcon(I(icon)), text, self)
ac.setObjectName('action-' + sid)
if target is not None:
ac.triggered.connect(target)
if isinstance(keys, type('')):
keys = (keys,)
@ -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()

View File

@ -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