mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
b5c0cc85d1
4339
imgsrc/console.svg
Normal file
4339
imgsrc/console.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 113 KiB |
BIN
resources/images/console.png
Normal file
BIN
resources/images/console.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
@ -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]
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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'] = (
|
||||
|
56
src/calibre/gui2/actions/tweak_epub.py
Executable file
56
src/calibre/gui2/actions/tweak_epub.py
Executable 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)
|
||||
|
84
src/calibre/gui2/dialogs/tweak_epub.py
Executable file
84
src/calibre/gui2/dialogs/tweak_epub.py
Executable 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)
|
||||
|
87
src/calibre/gui2/dialogs/tweak_epub.ui
Normal file
87
src/calibre/gui2/dialogs/tweak_epub.ui
Normal 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>&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>&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>&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>
|
@ -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'],
|
||||
|
@ -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,
|
||||
|
@ -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'])
|
||||
|
@ -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__
|
||||
|
362
src/calibre/utils/pyconsole/console.py
Normal file
362
src/calibre/utils/pyconsole/console.py
Normal 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)
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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_()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user