Add a source code editor to the comments editor

This commit is contained in:
Kovid Goyal 2010-12-20 00:08:40 -07:00
parent b358edb5e0
commit 4c564132ca

View File

@ -9,15 +9,16 @@ __docformat__ = 'restructuredtext en'
from lxml import html
from lxml.html import soupparser
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, \
QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, \
QSyntaxHighlighter, QColor, QChar
from PyQt4.QtWebKit import QWebView
from calibre.ebooks.chardet import xml_to_unicode
from calibre import xml_replace_entities
class PageAction(QAction):
class PageAction(QAction): # {{{
def __init__(self, wac, icon, text, checkable, view):
QAction.__init__(self, QIcon(I(icon+'.png')), text, view)
@ -41,8 +42,9 @@ class PageAction(QAction):
self.setChecked(self.page_action.isChecked())
self.setEnabled(self.page_action.isEnabled())
# }}}
class EditorWidget(QWebView):
class EditorWidget(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
@ -132,6 +134,219 @@ class EditorWidget(QWebView):
return property(fget=fget, fset=fset)
# }}}
# Highlighter {{{
State_Text = -1
State_DocType = 0
State_Comment = 1
State_TagStart = 2
State_TagName = 3
State_InsideTag = 4
State_AttributeName = 5
State_SingleQuote = 6
State_DoubleQuote = 7
State_AttributeValue = 8
class Highlighter(QSyntaxHighlighter):
def __init__(self, doc):
QSyntaxHighlighter.__init__(self, doc)
self.colors = {}
self.colors['doctype'] = QColor(192, 192, 192)
self.colors['entity'] = QColor(128, 128, 128)
self.colors['tag'] = QColor(136, 18, 128)
self.colors['comment'] = QColor( 35, 110, 37)
self.colors['attrname'] = QColor(153, 69, 0)
self.colors['attrval'] = QColor( 36, 36, 170)
def highlightBlock(self, text):
state = self.previousBlockState()
len_ = text.length()
start = 0
pos = 0
while pos < len_:
if state == State_Comment:
start = pos
while pos < len_:
if text.mid(pos, 3) == "-->":
pos += 3;
state = State_Text;
break
else:
pos += 1
self.setFormat(start, pos - start, self.colors['comment'])
elif state == State_DocType:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('>'):
state = State_Text
break
self.setFormat(start, pos - start, self.colors['doctype'])
# at '<' in e.g. "<span>foo</span>"
elif state == State_TagStart:
start = pos + 1
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('>'):
state = State_Text
break
if not ch.isSpace():
pos -= 1
state = State_TagName
break
# at 'b' in e.g "<blockquote>foo</blockquote>"
elif state == State_TagName:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch.isSpace():
pos -= 1
state = State_InsideTag
break
if ch == QChar('>'):
state = State_Text
break
self.setFormat(start, pos - start, self.colors['tag']);
# anywhere after tag name and before tag closing ('>')
elif state == State_InsideTag:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('/'):
continue
if ch == QChar('>'):
state = State_Text
break
if not ch.isSpace():
pos -= 1
state = State_AttributeName
break
# at 's' in e.g. <img src=bla.png/>
elif state == State_AttributeName:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('='):
state = State_AttributeValue
break
if ch in (QChar('>'), QChar('/')):
state = State_InsideTag
break
self.setFormat(start, pos - start, self.colors['attrname'])
# after '=' in e.g. <img src=bla.png/>
elif state == State_AttributeValue:
start = pos
# find first non-space character
while pos < len_:
ch = text.at(pos)
pos += 1
# handle opening single quote
if ch == QChar("'"):
state = State_SingleQuote
break
# handle opening double quote
if ch == QChar('"'):
state = State_DoubleQuote
break
if not ch.isSpace():
break
if state == State_AttributeValue:
# attribute value without quote
# just stop at non-space or tag delimiter
start = pos
while pos < len_:
ch = text.at(pos);
if ch.isSpace():
break
if ch in (QChar('>'), QChar('/')):
break
pos += 1
state = State_InsideTag
self.setFormat(start, pos - start, self.colors['attrval'])
# after the opening single quote in an attribute value
elif state == State_SingleQuote:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar("'"):
break
state = State_InsideTag
self.setFormat(start, pos - start, self.colors['attrval'])
# after the opening double quote in an attribute value
elif state == State_DoubleQuote:
start = pos
while pos < len_:
ch = text.at(pos)
pos += 1
if ch == QChar('"'):
break
state = State_InsideTag
self.setFormat(start, pos - start, self.colors['attrval'])
else:
# State_Text and default
while pos < len_:
ch = text.at(pos)
if ch == QChar('<'):
if text.mid(pos, 4) == "<!--":
state = State_Comment
else:
if text.mid(pos, 9).toUpper() == "<!DOCTYPE":
state = State_DocType
else:
state = State_TagStart
break;
elif ch == QChar('&'):
start = pos
while pos < len_ and text.at(pos) != QChar(';'):
self.setFormat(start, pos - start,
self.colors['entity'])
pos += 1
else:
pos += 1
self.setCurrentBlockState(state)
# }}}
class Editor(QWidget):
@ -140,12 +355,25 @@ class Editor(QWidget):
self.toolbar1 = QToolBar(self)
self.toolbar2 = QToolBar(self)
self.editor = EditorWidget(self)
self.tabs = QTabWidget(self)
self.tabs.setTabPosition(self.tabs.South)
self.wyswyg = QWidget(self.tabs)
self.code_edit = QPlainTextEdit(self.tabs)
self.source_dirty = False
self.wyswyg_dirty = True
self._layout = QVBoxLayout(self)
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
self.setLayout(self._layout)
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.addWidget(self.toolbar1)
self._layout.addWidget(self.toolbar2)
self._layout.addWidget(self.editor)
l.setContentsMargins(0, 0, 0, 0)
l.addWidget(self.toolbar1)
l.addWidget(self.toolbar2)
l.addWidget(self.editor)
self._layout.addWidget(self.tabs)
self.tabs.addTab(self.wyswyg, _('Normal view'))
self.tabs.addTab(self.code_edit, _('HTML Source'))
self.tabs.currentChanged[int].connect(self.change_tab)
self.highlighter = Highlighter(self.code_edit.document())
for x in ('bold', 'italic', 'underline', 'strikethrough',
'superscript', 'subscript', 'indent', 'outdent'):
@ -176,13 +404,32 @@ class Editor(QWidget):
self.toolbar1.addAction(ac)
self.toolbar1.addSeparator()
self.code_edit.textChanged.connect(self.code_dirtied)
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)
@dynamic_property
def html(self):
def fset(self, v):
self.editor.html = v
return property(fget=lambda self:self.editor.html, fset=fset)
def change_tab(self, index):
#print 'reloading:', (index and self.wyswyg_dirty) or (not index and
# self.source_dirty)
if index == 1: # changing to code view
if self.wyswyg_dirty:
self.code_edit.setPlainText(self.html)
self.wyswyg_dirty = False
elif index == 0: #changing to wyswyg
if self.source_dirty:
self.html = unicode(self.code_edit.toPlainText())
self.source_dirty = False
def wyswyg_dirtied(self, *args):
self.wyswyg_dirty = True
def code_dirtied(self, *args):
self.source_dirty = True
if __name__ == '__main__':
app = QApplication([])