mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add a source code editor to the comments editor
This commit is contained in:
parent
b358edb5e0
commit
4c564132ca
@ -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([])
|
||||
|
Loading…
x
Reference in New Issue
Block a user