Merge from trunk

This commit is contained in:
Charles Haley 2010-09-21 15:27:05 +01:00
commit b5c0cc85d1
16 changed files with 5004 additions and 258 deletions

4339
imgsrc/console.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -665,13 +665,17 @@ class ActionCopyToLibrary(InterfaceActionBase):
name = 'Copy To Library'
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
class ActionTweakEpub(InterfaceActionBase):
name = 'Tweak ePub'
actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction'
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
ActionCopyToLibrary]
ActionCopyToLibrary, ActionTweakEpub]
# }}}

View File

@ -740,7 +740,7 @@ class ITUNES(DriverBase):
# Purge the booklist, self.cached_books, thumb cache
for i,bl_book in enumerate(booklists[0]):
if DEBUG:
if False:
self.log.info(" evaluating '%s' by '%s' uuid:%s" %
(bl_book.title, bl_book.author,bl_book.uuid))

View File

@ -39,7 +39,7 @@ gprefs.defaults['action-layout-context-menu'] = (
'Edit Metadata', 'Send To Device', 'Save To Disk',
'Connect Share', 'Copy To Library', None,
'Convert Books', 'View', 'Open Folder', 'Show Book Details',
'Similar Books', None, 'Remove Books',
'Similar Books', 'Tweak ePub', None, 'Remove Books',
)
gprefs.defaults['action-layout-context-menu-device'] = (

View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.gui2 import error_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.tweak_epub import TweakEpub
class TweakEpubAction(InterfaceAction):
name = 'Tweak ePub'
action_spec = (_('Tweak ePub'), 'trim.png',
_('Make small changes to ePub format books'),
_('T'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.edit_epub_in_situ)
def edit_epub_in_situ(self, *args):
row = self.gui.library_view.currentIndex()
if not row.isValid():
return error_dialog(self.gui, _('Cannot tweak ePub'),
_('No book selected'), show=True)
# Confirm 'EPUB' in formats
book_id = self.gui.library_view.model().id(row)
try:
path_to_epub = self.gui.library_view.model().db.format_abspath(
book_id, 'EPUB', index_is_id=True)
except:
path_to_epub = None
if not path_to_epub:
return error_dialog(self.gui, _('Cannot tweak ePub'),
_('No ePub available. First convert the book to ePub.'),
show=True)
# Launch a modal dialog waiting for user to complete or cancel
dlg = TweakEpub(self.gui, path_to_epub)
if dlg.exec_() == dlg.Accepted:
self.update_db(book_id, dlg._output)
dlg.cleanup()
def update_db(self, book_id, rebuilt):
'''
Update the calibre db with the tweaked epub
'''
self.gui.library_view.model().db.add_format(book_id, 'EPUB',
open(rebuilt, 'rb'), index_is_id=True)

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, shutil
from contextlib import closing
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
from PyQt4.Qt import QDialog
from calibre.gui2 import open_local_file
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
from calibre.libunzip import extract as zipextract
from calibre.ptempfile import PersistentTemporaryDirectory
class TweakEpub(QDialog, Ui_Dialog):
'''
Display controls for tweaking ePubs
To do:
- need way to kill file browser proc in cleanup()
'''
def __init__(self, parent, epub):
QDialog.__init__(self, parent)
self._epub = epub
self._exploded = None
#self._file_browser_proc = None
self._output = None
# Run the dialog setup generated from tweak_epub.ui
self.setupUi(self)
self.cancel_button.clicked.connect(self.reject)
self.explode_button.clicked.connect(self.explode)
self.rebuild_button.clicked.connect(self.rebuild)
# Position update dialog overlaying top left of app window
parent_loc = parent.pos()
self.move(parent_loc.x(),parent_loc.y())
def cleanup(self):
# Delete directory containing exploded ePub
if self._exploded is not None:
shutil.rmtree(self._exploded, ignore_errors=True)
def display_exploded(self):
'''
Generic subprocess launch of native file browser
User can use right-click to 'Open with ...'
'''
open_local_file(self._exploded)
def explode(self, *args):
if self._exploded is None:
self._exploded = PersistentTemporaryDirectory("_exploded", prefix='')
zipextract(self._epub, self._exploded)
self.display_exploded()
self.rebuild_button.setEnabled(True)
self.explode_button.setEnabled(False)
def rebuild(self, *args):
self._output = os.path.join(self._exploded, 'rebuilt.epub')
with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf:
# Write mimetype
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
# Write everything else
exclude_files = ['.DS_Store','mimetype','iTunesMetadata.plist','rebuilt.epub']
for root, dirs, files in os.walk(self._exploded):
for fn in files:
if fn in exclude_files:
continue
absfn = os.path.join(root, fn)
zfn = os.path.relpath(absfn,
self._exploded).replace(os.sep, '/')
zf.write(absfn, zfn)
return QDialog.accept(self)

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>382</width>
<height>242</height>
</rect>
</property>
<property name="windowTitle">
<string>Tweak ePub</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="explode_button">
<property name="statusTip">
<string>Display contents of exploded ePub</string>
</property>
<property name="text">
<string>&amp;Explode ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="rebuild_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="statusTip">
<string>Rebuild ePub from exploded contents</string>
</property>
<property name="text">
<string>&amp;Rebuild ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/exec.png</normaloff>:/images/exec.png</iconset>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="cancel_button">
<property name="statusTip">
<string>Discard changes</string>
</property>
<property name="text">
<string>&amp;Cancel</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/window-close.png</normaloff>:/images/window-close.png</iconset>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>First, explode the epub. Then edit is contents by right clicking on the individual files and selecting the editor of your choice. When you are done, click rebuild epub and the epub in your calibre library will be updated with the changes you have made.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -217,6 +217,10 @@ def fetch_scheduled_recipe(arg):
if 'output_profile' in ps:
recs.append(('output_profile', ps['output_profile'],
OptionRecommendation.HIGH))
if ps['output_profile'] == 'kindle':
recs.append(('no_inline_toc', True,
OptionRecommendation.HIGH))
lf = load_defaults('look_and_feel')
if lf.get('base_font_size', 0.0) != 0.0:
recs.append(('base_font_size', lf['base_font_size'],

View File

@ -22,13 +22,15 @@ class Worker(object):
have the environment variable :envvar:`CALIBRE_WORKER` set.
Useful attributes: ``is_alive``, ``returncode``
usefule methods: ``kill``
Useful methods: ``kill``
To launch child simply call the Worker object. By default, the child's
output is redirected to an on disk file, the path to which is returned by
the call.
'''
exe_name = 'calibre-parallel'
@property
def osx_interpreter(self):
exe = os.path.basename(sys.executable)
@ -41,32 +43,33 @@ class Worker(object):
@property
def executable(self):
e = self.exe_name
if iswindows:
return os.path.join(os.path.dirname(sys.executable),
'calibre-parallel.exe' if isfrozen else \
'Scripts\\calibre-parallel.exe')
e+'.exe' if isfrozen else \
'Scripts\\%s.exe'%e)
if isnewosx:
return os.path.join(sys.console_binaries_path, 'calibre-parallel')
return os.path.join(sys.console_binaries_path, e)
if isosx:
if not isfrozen: return 'calibre-parallel'
if not isfrozen: return e
contents = os.path.join(self.osx_contents_dir,
'console.app', 'Contents')
return os.path.join(contents, 'MacOS', self.osx_interpreter)
if isfrozen:
return os.path.join(getattr(sys, 'frozen_path'), 'calibre-parallel')
return os.path.join(getattr(sys, 'frozen_path'), e)
c = os.path.join(sys.executables_location, 'calibre-parallel')
c = os.path.join(sys.executables_location, e)
if os.access(c, os.X_OK):
return c
return 'calibre-parallel'
return e
@property
def gui_executable(self):
if isnewosx:
return os.path.join(sys.binaries_path, 'calibre-parallel')
return os.path.join(sys.binaries_path, self.exe_name)
if isfrozen and isosx:
return os.path.join(self.osx_contents_dir,

View File

@ -80,8 +80,12 @@ def main():
if isosx and 'CALIBRE_WORKER_ADDRESS' not in os.environ:
# On some OS X computers launchd apparently tries to
# launch the last run process from the bundle
# so launch the gui as usual
from calibre.gui2.main import main as gui_main
return gui_main(['calibre'])
if 'CALIBRE_LAUNCH_INTERPRETER' in os.environ:
from calibre.utils.pyconsole.interpreter import main
return main()
address = cPickle.loads(unhexlify(os.environ['CALIBRE_WORKER_ADDRESS']))
key = unhexlify(os.environ['CALIBRE_WORKER_KEY'])
resultf = unhexlify(os.environ['CALIBRE_WORKER_RESULT'])

View File

@ -8,6 +8,15 @@ __docformat__ = 'restructuredtext en'
import sys
from calibre import prints as prints_
from calibre.utils.config import Config, StringConfig
def console_config(defaults=None):
desc=_('Settings to control the calibre content server')
c = Config('console', desc) if defaults is None else StringConfig(defaults, desc)
c.add_opt('--theme', default='default', help='The color theme')
def prints(*args, **kwargs):
kwargs['file'] = sys.__stdout__

View File

@ -0,0 +1,362 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, textwrap, traceback, StringIO
from PyQt4.Qt import QTextEdit, Qt, QTextFrameFormat, pyqtSignal, \
QCoreApplication
from pygments.lexers import PythonLexer, PythonTracebackLexer
from calibre.constants import __appname__, __version__
from calibre.utils.pyconsole.formatter import Formatter
from calibre.utils.pyconsole.repl import Interpreter, DummyFile
from calibre.utils.pyconsole import prints
from calibre.gui2 import error_dialog
class EditBlock(object): # {{{
def __init__(self, cursor):
self.cursor = cursor
def __enter__(self):
self.cursor.beginEditBlock()
return self.cursor
def __exit__(self, *args):
self.cursor.endEditBlock()
# }}}
class Prepender(object): # {{{
'Helper class to insert output before the current prompt'
def __init__(self, console):
self.console = console
def __enter__(self):
c = self.console
self.opos = c.cursor_pos
cur = c.prompt_frame.firstCursorPosition()
cur.movePosition(cur.PreviousCharacter)
c.setTextCursor(cur)
def __exit__(self, *args):
self.console.cursor_pos = self.opos
# }}}
class Console(QTextEdit):
running = pyqtSignal()
running_done = pyqtSignal()
@property
def doc(self):
return self.document()
@property
def cursor(self):
return self.textCursor()
@property
def root_frame(self):
return self.doc.rootFrame()
def unhandled_exception(self, type, value, tb):
if type == KeyboardInterrupt:
return
try:
sio = StringIO.StringIO()
traceback.print_exception(type, value, tb, file=sio)
fe = sio.getvalue()
prints(fe)
try:
val = unicode(value)
except:
val = repr(value)
msg = '<b>%s</b>:'%type.__name__ + val
error_dialog(self, _('ERROR: Unhandled exception'), msg,
det_msg=fe, show=True)
except BaseException:
pass
def __init__(self,
prompt='>>> ',
continuation='... ',
parent=None):
QTextEdit.__init__(self, parent)
self.buf = []
self.prompt_frame = None
self.allow_output = False
self.prompt_frame_format = QTextFrameFormat()
self.prompt_frame_format.setBorder(1)
self.prompt_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
self.prompt_len = len(prompt)
self.doc.setMaximumBlockCount(10000)
self.lexer = PythonLexer(ensurenl=False)
self.tb_lexer = PythonTracebackLexer()
self.formatter = Formatter(prompt, continuation, style='default')
self.setStyleSheet(self.formatter.stylesheet)
self.key_dispatcher = { # {{{
Qt.Key_Enter : self.enter_pressed,
Qt.Key_Return : self.enter_pressed,
Qt.Key_Home : self.home_pressed,
Qt.Key_End : self.end_pressed,
Qt.Key_Left : self.left_pressed,
Qt.Key_Right : self.right_pressed,
} # }}}
motd = textwrap.dedent('''\
# Python {0}
# {1} {2}
'''.format(sys.version.splitlines()[0], __appname__,
__version__))
with EditBlock(self.cursor):
self.render_block(motd)
sys.stdout = sys.stderr = DummyFile(parent=self)
sys.stdout.write_output.connect(self.show_output)
self.interpreter = Interpreter(parent=self)
self.interpreter.show_error.connect(self.show_error)
sys.excepthook = self.unhandled_exception
# Prompt management {{{
@dynamic_property
def cursor_pos(self):
doc = '''
The cursor position in the prompt has the form (row, col).
row starts at 0 for the first line
col is 0 if the cursor is at the start of the line, 1 if it is after
the first character, n if it is after the nth char.
'''
def fget(self):
if self.prompt_frame is not None:
pos = self.cursor.position()
it = self.prompt_frame.begin()
lineno = 0
while not it.atEnd():
bl = it.currentBlock()
if bl.contains(pos):
return (lineno, pos - bl.position())
it += 1
lineno += 1
return (-1, -1)
def fset(self, val):
row, col = val
if self.prompt_frame is not None:
it = self.prompt_frame.begin()
lineno = 0
while not it.atEnd():
if lineno == row:
c = self.cursor
c.setPosition(it.currentBlock().position())
c.movePosition(c.NextCharacter, n=col)
self.setTextCursor(c)
break
it += 1
lineno += 1
return property(fget=fget, fset=fset, doc=doc)
def prompt(self, strip_prompt_strings=True):
if not self.prompt_frame:
yield u'' if strip_prompt_strings else self.formatter.prompt
else:
it = self.prompt_frame.begin()
while not it.atEnd():
bl = it.currentBlock()
t = unicode(bl.text())
if strip_prompt_strings:
t = t[self.prompt_len:]
yield t
it += 1
def set_prompt(self, lines):
self.render_current_prompt(lines)
def clear_current_prompt(self):
if self.prompt_frame is None:
c = self.root_frame.lastCursorPosition()
self.prompt_frame = c.insertFrame(self.prompt_frame_format)
self.setTextCursor(c)
else:
c = self.prompt_frame.firstCursorPosition()
self.setTextCursor(c)
c.setPosition(self.prompt_frame.lastPosition(), c.KeepAnchor)
c.removeSelectedText()
c.setPosition(self.prompt_frame.firstPosition())
def render_current_prompt(self, lines=None, restore_cursor=False):
row, col = self.cursor_pos
cp = list(self.prompt()) if lines is None else lines
self.clear_current_prompt()
for i, line in enumerate(cp):
start = i == 0
end = i == len(cp) - 1
self.formatter.render_prompt(not start, self.cursor)
self.formatter.render(self.lexer.get_tokens(line), self.cursor)
if not end:
self.cursor.insertBlock()
if row > -1 and restore_cursor:
self.cursor_pos = (row, col)
self.ensureCursorVisible()
# }}}
# Non-prompt Rendering {{{
def render_block(self, text, restore_prompt=True):
self.formatter.render(self.lexer.get_tokens(text), self.cursor)
self.cursor.insertBlock()
self.cursor.movePosition(self.cursor.End)
if restore_prompt:
self.render_current_prompt()
def show_error(self, is_syntax_err, tb):
if self.prompt_frame is not None:
# At a prompt, so redirect output
return prints(tb, end='')
try:
self.buf.append(tb)
if is_syntax_err:
self.formatter.render_syntax_error(tb, self.cursor)
else:
self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor)
except:
prints(tb, end='')
self.ensureCursorVisible()
QCoreApplication.processEvents()
def show_output(self, raw):
def do_show():
try:
self.buf.append(raw)
self.formatter.render_raw(raw, self.cursor)
except:
import traceback
prints(traceback.format_exc())
prints(raw, end='')
if self.prompt_frame is not None:
with Prepender(self):
do_show()
else:
do_show()
self.ensureCursorVisible()
QCoreApplication.processEvents()
# }}}
# Keyboard handling {{{
def keyPressEvent(self, ev):
text = unicode(ev.text())
key = ev.key()
action = self.key_dispatcher.get(key, None)
if callable(action):
action()
elif key in (Qt.Key_Escape,):
QTextEdit.keyPressEvent(self, ev)
elif text:
self.text_typed(text)
else:
QTextEdit.keyPressEvent(self, ev)
def left_pressed(self):
lineno, pos = self.cursor_pos
if lineno < 0: return
if pos > self.prompt_len:
c = self.cursor
c.movePosition(c.PreviousCharacter)
self.setTextCursor(c)
elif lineno > 0:
c = self.cursor
c.movePosition(c.Up)
c.movePosition(c.EndOfLine)
self.setTextCursor(c)
self.ensureCursorVisible()
def right_pressed(self):
lineno, pos = self.cursor_pos
if lineno < 0: return
c = self.cursor
lineno, pos = self.cursor_pos
cp = list(self.prompt(False))
if pos < len(cp[lineno]):
c.movePosition(c.NextCharacter)
elif lineno < len(cp)-1:
c.movePosition(c.NextCharacter, n=1+self.prompt_len)
self.setTextCursor(c)
self.ensureCursorVisible()
def home_pressed(self):
if self.prompt_frame is not None:
c = self.cursor
c.movePosition(c.StartOfLine)
c.movePosition(c.NextCharacter, n=self.prompt_len)
self.setTextCursor(c)
self.ensureCursorVisible()
def end_pressed(self):
if self.prompt_frame is not None:
c = self.cursor
c.movePosition(c.EndOfLine)
self.setTextCursor(c)
self.ensureCursorVisible()
def enter_pressed(self):
if self.prompt_frame is None:
return
cp = list(self.prompt())
if cp[0]:
c = self.root_frame.lastCursorPosition()
self.setTextCursor(c)
old_pf = self.prompt_frame
self.prompt_frame = None
oldbuf = self.buf
self.buf = []
self.running.emit()
try:
ret = self.interpreter.runsource('\n'.join(cp))
except SystemExit:
ret = False
self.show_output('Raising SystemExit not allowed\n')
self.running_done.emit()
if ret: # Incomplete command
self.buf = oldbuf
self.prompt_frame = old_pf
c = old_pf.lastCursorPosition()
c.insertBlock()
self.setTextCursor(c)
else: # Command completed
try:
old_pf.setFrameFormat(QTextFrameFormat())
except RuntimeError:
# Happens if enough lines of output that the old
# frame was deleted
pass
self.render_current_prompt()
def text_typed(self, text):
if self.prompt_frame is not None:
self.cursor.insertText(text)
self.render_current_prompt(restore_cursor=True)
# }}}

View File

@ -1,226 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, textwrap
from PyQt4.Qt import QTextEdit, Qt, QTextFrameFormat
from pygments.lexers import PythonLexer, PythonTracebackLexer
from calibre.constants import __appname__, __version__
from calibre.utils.pyconsole.formatter import Formatter
from calibre.utils.pyconsole.repl import Interpreter, DummyFile
from calibre.utils.pyconsole import prints
class EditBlock(object): # {{{
def __init__(self, cursor):
self.cursor = cursor
def __enter__(self):
self.cursor.beginEditBlock()
return self.cursor
def __exit__(self, *args):
self.cursor.endEditBlock()
# }}}
class Editor(QTextEdit):
@property
def doc(self):
return self.document()
@property
def cursor(self):
return self.textCursor()
@property
def root_frame(self):
return self.doc.rootFrame()
@property
def cursor_pos(self):
pass
#pos = self.cursor.position() - self.prompt_frame.firstPosition()
#i = 0
#for line in self.current_prompt:
# i += self.prompt_len
def __init__(self,
prompt='>>> ',
continuation='... ',
parent=None):
QTextEdit.__init__(self, parent)
self.buf = ''
self.prompt_frame = None
self.current_prompt = ['']
self.allow_output = False
self.prompt_frame_format = QTextFrameFormat()
self.prompt_frame_format.setBorder(1)
self.prompt_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
self.prompt_len = len(prompt)
self.doc.setMaximumBlockCount(10000)
self.lexer = PythonLexer(ensurenl=False)
self.tb_lexer = PythonTracebackLexer()
self.formatter = Formatter(prompt, continuation)
motd = textwrap.dedent('''\
# Python {0}
# {1} {2}
'''.format(sys.version.splitlines()[0], __appname__,
__version__))
with EditBlock(self.cursor):
self.render_block(motd)
sys.stdout = sys.stderr = DummyFile(parent=self)
sys.stdout.write_output.connect(self.show_output)
self.interpreter = Interpreter(parent=self)
self.interpreter.show_error.connect(self.show_error)
#it = self.prompt_frame.begin()
#while not it.atEnd():
# bl = it.currentBlock()
# prints(repr(bl.text()))
# it += 1
# Rendering {{{
def render_block(self, text, restore_prompt=True):
self.formatter.render(self.lexer.get_tokens(text), self.cursor)
self.cursor.insertBlock()
self.cursor.movePosition(self.cursor.End)
if restore_prompt:
self.render_current_prompt()
def clear_current_prompt(self):
if self.prompt_frame is None:
c = self.root_frame.lastCursorPosition()
self.prompt_frame = c.insertFrame(self.prompt_frame_format)
self.setTextCursor(c)
else:
c = self.prompt_frame.firstCursorPosition()
self.setTextCursor(c)
c.setPosition(self.prompt_frame.lastPosition(), c.KeepAnchor)
c.removeSelectedText()
c.setPosition(self.prompt_frame.firstPosition())
def render_current_prompt(self):
self.clear_current_prompt()
for i, line in enumerate(self.current_prompt):
start = i == 0
end = i == len(self.current_prompt) - 1
self.formatter.render_prompt(not start, self.cursor)
self.formatter.render(self.lexer.get_tokens(line), self.cursor)
if not end:
self.cursor.insertText('\n')
def show_error(self, is_syntax_err, tb):
if self.prompt_frame is not None:
# At a prompt, so redirect output
return prints(tb)
try:
self.buf += tb
if is_syntax_err:
self.formatter.render_syntax_error(tb, self.cursor)
else:
self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor)
except:
prints(tb)
def show_output(self, raw):
if self.prompt_frame is not None:
# At a prompt, so redirect output
return prints(raw)
try:
self.current_prompt_range = None
self.buf += raw
self.formatter.render_raw(raw, self.cursor)
except:
prints(raw)
# }}}
# Keyboard handling {{{
def keyPressEvent(self, ev):
text = unicode(ev.text())
key = ev.key()
if key in (Qt.Key_Enter, Qt.Key_Return):
self.enter_pressed()
elif key == Qt.Key_Home:
self.home_pressed()
elif key == Qt.Key_End:
self.end_pressed()
elif key == Qt.Key_Left:
self.left_pressed()
elif key == Qt.Key_Right:
self.right_pressed()
elif text:
self.text_typed(text)
else:
QTextEdit.keyPressEvent(self, ev)
def left_pressed(self):
pass
def right_pressed(self):
if self.prompt_frame is not None:
c = self.cursor
c.movePosition(c.NextCharacter)
self.setTextCursor(c)
def home_pressed(self):
if self.prompt_frame is not None:
c = self.cursor
c.movePosition(c.StartOfLine)
c.movePosition(c.NextCharacter, n=self.prompt_len)
self.setTextCursor(c)
def end_pressed(self):
if self.prompt_frame is not None:
c = self.cursor
c.movePosition(c.EndOfLine)
self.setTextCursor(c)
def enter_pressed(self):
if self.prompt_frame is None:
return
if self.current_prompt[0]:
c = self.root_frame.lastCursorPosition()
self.setTextCursor(c)
old_pf = self.prompt_frame
self.prompt_frame = None
oldbuf = self.buf
self.buf = ''
ret = self.interpreter.runsource('\n'.join(self.current_prompt))
if ret: # Incomplete command
self.buf = oldbuf
self.prompt_frame = old_pf
self.current_prompt.append('')
else: # Command completed
self.current_prompt = ['']
old_pf.setFrameFormat(QTextFrameFormat())
self.render_current_prompt()
def text_typed(self, text):
if not self.current_prompt[0]:
self.cursor.beginEditBlock()
else:
self.cursor.joinPreviousEditBlock()
self.current_prompt[-1] += text
self.render_current_prompt()
self.cursor.endEditBlock()
# }}}

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QTextCharFormat, QFont, QBrush, QColor
from pygments.formatter import Formatter as PF
from pygments.token import Token
from pygments.token import Token, Generic
class Formatter(object):
@ -22,11 +22,16 @@ class Formatter(object):
pf = PF(**options)
self.styles = {}
self.normal = self.base_fmt()
self.background_color = pf.style.background_color
self.color = 'black'
for ttype, ndef in pf.style:
fmt = self.base_fmt()
if ndef['color']:
fmt.setForeground(QBrush(QColor('#%s'%ndef['color'])))
fmt.setUnderlineColor(QColor('#%s'%ndef['color']))
if ttype == Generic.Output:
self.color = '#%s'%ndef['color']
if ndef['bold']:
fmt.setFontWeight(QFont.Bold)
if ndef['italic']:
@ -40,6 +45,11 @@ class Formatter(object):
self.styles[ttype] = fmt
self.stylesheet = '''
QTextEdit { color: %s; background-color: %s }
'''%(self.color, self.background_color)
def base_fmt(self):
fmt = QTextCharFormat()
fmt.setFontFamily('monospace')
@ -74,7 +84,7 @@ class Formatter(object):
def render_prompt(self, is_continuation, cursor):
pr = self.continuation if is_continuation else self.prompt
fmt = self.styles[Token.Generic.Subheading]
fmt = self.styles[Generic.Prompt]
cursor.insertText(pr, fmt)

View File

@ -6,19 +6,31 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
__version__ = '0.1.0'
from PyQt4.Qt import QMainWindow, QToolBar, QStatusBar, QLabel, QFont, Qt, \
QApplication
from functools import partial
from PyQt4.Qt import QDialog, QToolBar, QStatusBar, QLabel, QFont, Qt, \
QApplication, QIcon, QVBoxLayout
from calibre.constants import __appname__, __version__
from calibre.utils.pyconsole.editor import Editor
from calibre.utils.pyconsole.console import Console
class MainWindow(QMainWindow):
class MainWindow(QDialog):
def __init__(self, default_status_msg):
def __init__(self,
default_status_msg=_('Welcome to') + ' ' + __appname__+' console',
parent=None):
QMainWindow.__init__(self)
QDialog.__init__(self, parent)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.resize(600, 700)
self.resize(800, 600)
# Setup tool bar {{{
self.tool_bar = QToolBar(self)
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextOnly)
self.l.addWidget(self.tool_bar)
# }}}
# Setup status bar {{{
self.status_bar = QStatusBar(self)
@ -28,25 +40,23 @@ class MainWindow(QMainWindow):
self.status_bar._font.setBold(True)
self.status_bar.defmsg.setFont(self.status_bar._font)
self.status_bar.addWidget(self.status_bar.defmsg)
self.setStatusBar(self.status_bar)
# }}}
# Setup tool bar {{{
self.tool_bar = QToolBar(self)
self.addToolBar(Qt.BottomToolBarArea, self.tool_bar)
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextOnly)
# }}}
self.editor = Editor(parent=self)
self.setCentralWidget(self.editor)
self.console = Console(parent=self)
self.console.running.connect(partial(self.status_bar.showMessage,
_('Code is running')))
self.console.running_done.connect(self.status_bar.clearMessage)
self.l.addWidget(self.console)
self.l.addWidget(self.status_bar)
self.setWindowTitle(__appname__ + ' console')
self.setWindowIcon(QIcon(I('console.png')))
def main():
QApplication.setApplicationName(__appname__+' console')
QApplication.setOrganizationName('Kovid Goyal')
app = QApplication([])
m = MainWindow(_('Welcome to') + ' ' + __appname__+' console')
m = MainWindow()
m.show()
app.exec_()