From 2e8a1be0c38981cd0928d355678a75dffec2bafe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Nov 2013 12:13:47 +0530 Subject: [PATCH] Start integrating the editor component into the main gui --- src/calibre/gui2/tweak_book/boss.py | 92 ++++++++++++------- .../gui2/tweak_book/editor/__init__.py | 21 +++++ .../gui2/tweak_book/editor/syntax/css.py | 2 +- .../gui2/tweak_book/editor/syntax/html.py | 14 ++- src/calibre/gui2/tweak_book/editor/text.py | 28 +----- src/calibre/gui2/tweak_book/editor/widget.py | 41 +++++++++ src/calibre/gui2/tweak_book/file_list.py | 14 ++- src/calibre/gui2/tweak_book/ui.py | 45 ++++++++- 8 files changed, 194 insertions(+), 63 deletions(-) create mode 100644 src/calibre/gui2/tweak_book/editor/widget.py diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index b2084a3820..5a4f9e74d9 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -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() + # }}} + diff --git a/src/calibre/gui2/tweak_book/editor/__init__.py b/src/calibre/gui2/tweak_book/editor/__init__.py index 2dfbaa2fb8..336ce21a37 100644 --- a/src/calibre/gui2/tweak_book/editor/__init__.py +++ b/src/calibre/gui2/tweak_book/editor/__init__.py @@ -6,5 +6,26 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' +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) diff --git a/src/calibre/gui2/tweak_book/editor/syntax/css.py b/src/calibre/gui2/tweak_book/editor/syntax/css.py index 3b836e8f64..5de019d172 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/css.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/css.py @@ -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 */ diff --git a/src/calibre/gui2/tweak_book/editor/syntax/html.py b/src/calibre/gui2/tweak_book/editor/syntax/html.py index 189c67706e..94479e4a5b 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/html.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/html.py @@ -7,6 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' 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 ' 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('''\ diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 78a40606c8..61e1f9eef7 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -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_() - - diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py new file mode 100644 index 0000000000..0e4c7b482c --- /dev/null +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -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 ' + +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_() + diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 48fffc469a..404b9128de 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -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)) diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 2714120d7b..8a9e04670a 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -6,7 +6,9 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' -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('

'+_( + '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')