Start integrating the editor component into the main gui

This commit is contained in:
Kovid Goyal 2013-11-02 12:13:47 +05:30
parent 95d2f0feab
commit 2e8a1be0c3
8 changed files with 194 additions and 63 deletions

View File

@ -24,6 +24,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.tweak_book import set_current_container, current_container, tprefs
from calibre.gui2.tweak_book.undo import GlobalUndoHistory
from calibre.gui2.tweak_book.save import SaveManager
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
def get_container(*args, **kwargs):
kwargs['tweak_mode'] = True
@ -39,6 +40,7 @@ class Boss(QObject):
self.save_manager = SaveManager(parent)
self.save_manager.report_error.connect(self.report_save_error)
self.doing_terminal_save = False
self.editors = {}
def __call__(self, gui):
self.gui = gui
@ -46,6 +48,7 @@ class Boss(QObject):
fl.delete_requested.connect(self.delete_requested)
fl.reorder_spine.connect(self.reorder_spine)
fl.rename_requested.connect(self.rename_requested)
fl.edit_file.connect(self.edit_file_requested)
def mkdtemp(self):
self.container_count += 1
@ -97,25 +100,6 @@ class Boss(QObject):
self.gui.action_save.setEnabled(False)
self.update_global_history_actions()
def update_global_history_actions(self):
gu = self.global_undo
for x, text in (('undo', _('&Revert to before')), ('redo', '&Revert to after')):
ac = getattr(self.gui, 'action_global_%s' % x)
ac.setEnabled(getattr(gu, 'can_' + x))
ac.setText(text + ' ' + (getattr(gu, x + '_msg') or '...'))
def add_savepoint(self, msg):
nc = clone_container(current_container(), self.mkdtemp())
self.global_undo.add_savepoint(nc, msg)
set_current_container(nc)
self.update_global_history_actions()
def rewind_savepoint(self):
container = self.global_undo.rewind_savepoint()
if container is not None:
set_current_container(container)
self.update_global_history_actions()
def apply_container_update_to_gui(self):
container = current_container()
self.gui.file_list.build(container)
@ -123,18 +107,6 @@ class Boss(QObject):
self.gui.action_save.setEnabled(True)
# TODO: Apply to other GUI elements
def do_global_undo(self):
container = self.global_undo.undo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()
def do_global_redo(self):
container = self.global_undo.redo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()
def delete_requested(self, spine_items, other_items):
if not self.check_dirtied():
return
@ -157,6 +129,7 @@ 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
# Renaming {{{
def rename_requested(self, oldname, newname):
if not self.check_dirtied():
return
@ -191,6 +164,40 @@ class Boss(QObject):
self.gui.file_list.build(current_container())
self.gui.action_save.setEnabled(True)
# TODO: Update the rest of the GUI
# }}}
# Global history {{{
def do_global_undo(self):
container = self.global_undo.undo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()
def do_global_redo(self):
container = self.global_undo.redo()
if container is not None:
set_current_container(container)
self.apply_container_update_to_gui()
def update_global_history_actions(self):
gu = self.global_undo
for x, text in (('undo', _('&Revert to before')), ('redo', '&Revert to after')):
ac = getattr(self.gui, 'action_global_%s' % x)
ac.setEnabled(getattr(gu, 'can_' + x))
ac.setText(text + ' ' + (getattr(gu, x + '_msg') or '...'))
def add_savepoint(self, msg):
nc = clone_container(current_container(), self.mkdtemp())
self.global_undo.add_savepoint(nc, msg)
set_current_container(nc)
self.update_global_history_actions()
def rewind_savepoint(self):
container = self.global_undo.rewind_savepoint()
if container is not None:
set_current_container(container)
self.update_global_history_actions()
# }}}
def save_book(self):
self.gui.action_save.setEnabled(False)
@ -206,6 +213,27 @@ class Boss(QObject):
_('Saving of the book failed. Click "Show Details"'
' for more information.'), det_msg=tb, show=True)
def edit_file(self, name, syntax):
editor = self.editors.get(name, None)
if editor is None:
editor = self.editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
self.gui.central.add_editor(name, editor)
c = current_container()
editor.load_text(c.decode(c.open(name).read()), syntax=syntax)
self.gui.central.show_editor(editor)
def edit_file_requested(self, name, syntax, mime):
if name in self.editors:
self.gui.show_editor(self.editors[name])
return
syntax = syntax or syntax_from_mime(mime)
if not syntax:
return error_dialog(
self.gui, _('Unsupported file format'),
_('Editing of files of type %s is not supported' % mime), show=True)
self.edit_file(name, syntax)
# Shutdown {{{
def quit(self):
if not self.confirm_quit():
return
@ -275,3 +303,5 @@ class Boss(QObject):
def save_state(self):
with tprefs:
self.gui.save_state()
# }}}

View File

@ -6,5 +6,26 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.polish.container import guess_type
def syntax_from_mime(mime):
if mime in OEB_DOCS:
return 'html'
if mime in OEB_STYLES:
return 'css'
if mime == guess_type('a.opf'):
return 'xml'
if mime == guess_type('a.ncx'):
return 'xml'
if mime == guess_type('a.xml'):
return 'xml'
if mime.startswith('text/'):
return 'text'
def editor_from_syntax(syntax, parent=None):
if syntax not in {'text', 'html', 'css', 'xml'}:
return None
from calibre.gui2.tweak_book.editor.widget import Editor
return Editor(parent)

View File

@ -254,7 +254,7 @@ class CSSHighlighter(SyntaxHighlighter):
create_formats_func = create_formats
if __name__ == '__main__':
from calibre.gui2.tweak_book.editor.text import launch_editor
from calibre.gui2.tweak_book.editor.widget import launch_editor
launch_editor('''\
@charset "utf-8";
/* A demonstration css sheet */

View File

@ -7,6 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from functools import partial
from PyQt4.Qt import (QTextCharFormat, QFont)
@ -166,7 +167,7 @@ def normal(state, text, i, formats):
t = normal_pat.search(text, i).group()
return mark_nbsp(state, t, formats['nbsp'])
def opening_tag(state, text, i, formats):
def opening_tag(cdata_tags, state, text, i, formats):
'An opening tag, like <a>'
ch = text[i]
if ch in space_chars:
@ -270,7 +271,7 @@ def in_comment(state, text, i, formats):
state_map = {
State.NORMAL:normal,
State.IN_OPENING_TAG: opening_tag,
State.IN_OPENING_TAG: partial(opening_tag, cdata_tags),
State.IN_CLOSING_TAG: closing_tag,
State.ATTRIBUTE_NAME: attribute_name,
State.ATTRIBUTE_VALUE: attribute_value,
@ -284,6 +285,9 @@ for x in (State.IN_COMMENT, State.IN_PI, State.IN_DOCTYPE):
for x in (State.SQ_VAL, State.DQ_VAL):
state_map[x] = quoted_val
xml_state_map = state_map.copy()
xml_state_map[State.IN_OPENING_TAG] = partial(opening_tag, set())
def create_formats(highlighter):
t = highlighter.theme
formats = {
@ -332,8 +336,12 @@ class HTMLHighlighter(SyntaxHighlighter):
ans.css_formats = self.css_formats
return ans
class XMLHighlighter(HTMLHighlighter):
state_map = xml_state_map
if __name__ == '__main__':
from calibre.gui2.tweak_book.editor.text import launch_editor
from calibre.gui2.tweak_book.editor.widget import launch_editor
launch_editor('''\
<!DOCTYPE html>
<html xml:lang="en" lang="en">

View File

@ -10,13 +10,13 @@ import textwrap
from future_builtins import map
from PyQt4.Qt import (
QPlainTextEdit, QApplication, QFontDatabase, QToolTip, QPalette, QFont,
QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, QDialog, QVBoxLayout)
QPlainTextEdit, QFontDatabase, QToolTip, QPalette, QFont,
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.syntax.base import SyntaxHighlighter
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
from calibre.gui2.tweak_book.editor.syntax.css import CSSHighlighter
_dff = None
@ -94,7 +94,7 @@ class TextEdit(QPlainTextEdit):
# }}}
def load_text(self, text, syntax='html'):
self.highlighter = {'html':HTMLHighlighter, 'css':CSSHighlighter}.get(syntax, SyntaxHighlighter)(self)
self.highlighter = {'html':HTMLHighlighter, 'css':CSSHighlighter, 'xml':XMLHighlighter}.get(syntax, SyntaxHighlighter)(self)
self.highlighter.apply_theme(self.theme)
self.highlighter.setDocument(self.document())
self.setPlainText(text)
@ -200,23 +200,3 @@ class TextEdit(QPlainTextEdit):
ev.ignore()
# }}}
def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):
if path_is_raw:
raw = path_to_edit
else:
with open(path_to_edit, 'rb') as f:
raw = f.read().decode('utf-8')
ext = path_to_edit.rpartition('.')[-1].lower()
if ext in ('html', 'htm', 'xhtml', 'xhtm'):
syntax = 'html'
elif ext in ('css',):
syntax = 'css'
app = QApplication([]) # noqa
t = TextEdit()
t.load_text(raw, syntax=syntax)
d = QDialog()
d.setLayout(QVBoxLayout())
d.layout().addWidget(t)
d.exec_()

View File

@ -0,0 +1,41 @@
#!/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 QMainWindow, Qt, QApplication
from calibre.gui2.tweak_book.editor.text import TextEdit
class Editor(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
if parent is None:
self.setWindowFlags(Qt.Widget)
self.editor = TextEdit(self)
self.setCentralWidget(self.editor)
def load_text(self, raw, syntax='html'):
self.editor.load_text(raw, syntax=syntax)
def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):
if path_is_raw:
raw = path_to_edit
else:
with open(path_to_edit, 'rb') as f:
raw = f.read().decode('utf-8')
ext = path_to_edit.rpartition('.')[-1].lower()
if ext in ('html', 'htm', 'xhtml', 'xhtm'):
syntax = 'html'
elif ext in ('css',):
syntax = 'css'
app = QApplication([])
t = Editor()
t.load_text(raw, syntax=syntax)
t.show()
app.exec_()

View File

@ -23,6 +23,7 @@ TOP_ICON_SIZE = 24
NAME_ROLE = Qt.UserRole
CATEGORY_ROLE = NAME_ROLE + 1
LINEAR_ROLE = CATEGORY_ROLE + 1
MIME_ROLE = LINEAR_ROLE + 1
NBSP = '\xa0'
class ItemDelegate(QStyledItemDelegate): # {{{
@ -71,6 +72,7 @@ class FileList(QTreeWidget):
delete_requested = pyqtSignal(object, object)
reorder_spine = pyqtSignal(object)
rename_requested = pyqtSignal(object, object)
edit_file = pyqtSignal(object, object, object)
def __init__(self, parent=None):
QTreeWidget.__init__(self, parent)
@ -106,6 +108,7 @@ class FileList(QTreeWidget):
'misc':'mimetypes/dir.png',
'images':'view-image.png',
}.iteritems()}
self.itemDoubleClicked.connect(self.item_double_clicked)
def get_state(self):
s = {'pos':self.verticalScrollBar().value()}
@ -232,6 +235,7 @@ class FileList(QTreeWidget):
item.setData(0, NAME_ROLE, name)
item.setData(0, CATEGORY_ROLE, category)
item.setData(0, LINEAR_ROLE, bool(linear))
item.setData(0, MIME_ROLE, imt)
set_display_name(name, item)
# TODO: Add appropriate tooltips based on the emblems
emblems = []
@ -327,11 +331,19 @@ class FileList(QTreeWidget):
order[i][1] = True
self.reorder_spine.emit(order)
def item_double_clicked(self, item, column):
category = unicode(item.data(0, CATEGORY_ROLE).toString())
mime = unicode(item.data(0, MIME_ROLE).toString())
name = unicode(item.data(0, NAME_ROLE).toString())
syntax = {'text':'html', 'styles':'css'}.get(category, None)
self.edit_file.emit(name, syntax, mime)
class FileListWidget(QWidget):
delete_requested = pyqtSignal(object, object)
reorder_spine = pyqtSignal(object)
rename_requested = pyqtSignal(object, object)
edit_file = pyqtSignal(object, object, object)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
@ -339,7 +351,7 @@ class FileListWidget(QWidget):
self.file_list = FileList(self)
self.layout().addWidget(self.file_list)
self.layout().setContentsMargins(0, 0, 0, 0)
for x in ('delete_requested', 'reorder_spine', 'rename_requested'):
for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file'):
getattr(self.file_list, x).connect(getattr(self, x))
for x in ('delete_done',):
setattr(self, x, getattr(self.file_list, x))

View File

@ -6,7 +6,9 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import QDockWidget, Qt, QLabel, QIcon, QAction, QApplication
from PyQt4.Qt import (
QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget,
QVBoxLayout, QStackedWidget, QTabWidget)
from calibre.constants import __appname__, get_version
from calibre.gui2.main_window import MainWindow
@ -16,6 +18,38 @@ from calibre.gui2.tweak_book.job import BlockingJob
from calibre.gui2.tweak_book.boss import Boss
from calibre.gui2.keyboard import Manager as KeyboardManager
class Central(QStackedWidget):
' The central widget, hosts the editors '
def __init__(self, parent=None):
QStackedWidget.__init__(self, parent)
self.welcome = w = QLabel('<p>'+_(
'Double click a file in the left panel to start editing'
' it.'))
self.addWidget(w)
w.setWordWrap(True)
w.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
self.container = c = QWidget(self)
self.addWidget(c)
l = c.l = QVBoxLayout(c)
c.setLayout(l)
l.setContentsMargins(0, 0, 0, 0)
self.editor_tabs = t = QTabWidget(c)
l.addWidget(t)
t.setDocumentMode(True)
t.setTabsClosable(True)
t.setMovable(True)
def add_editor(self, name, editor):
fname = name.rpartition('/')[2]
index = self.editor_tabs.addTab(editor, fname)
self.editor_tabs.setTabToolTip(index, name)
def show_editor(self, editor):
self.setCurrentIndex(1)
self.editor_tabs.setCurrentWidget(editor)
class Main(MainWindow):
APP_NAME = _('Tweak Book')
@ -39,14 +73,15 @@ class Main(MainWindow):
self.create_docks()
self.status_bar = self.statusBar()
self.l = QLabel('Placeholder')
self.status_bar.addPermanentWidget(self.boss.save_manager.status_widget)
self.status_bar.addWidget(QLabel(_('{0} {1} created by {2}').format(__appname__, get_version(), 'Kovid Goyal')))
f = self.status_bar.font()
f.setBold(True)
self.status_bar.setFont(f)
self.setCentralWidget(self.l)
self.central = Central(self)
self.setCentralWidget(self.central)
self.boss(self)
g = QApplication.instance().desktop().availableGeometry(self)
self.resize(g.width()-50, g.height()-50)
@ -54,6 +89,10 @@ class Main(MainWindow):
self.keyboard.finalize()
@property
def editor_tabs(self):
return self.central.editor_tabs
def create_actions(self):
group = _('Global Actions')