1
0

fb2code.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. #include "fb2code.hpp"
  2. #include "fb2dlgs.hpp"
  3. //---------------------------------------------------------------------------
  4. // FbHighlighter
  5. //---------------------------------------------------------------------------
  6. #include <QXmlInputSource>
  7. #include <QtGui>
  8. static const QColor DEFAULT_SYNTAX_CHAR = Qt::blue;
  9. static const QColor DEFAULT_ELEMENT_NAME = Qt::darkRed;
  10. static const QColor DEFAULT_COMMENT = Qt::darkGray;
  11. static const QColor DEFAULT_ATTRIBUTE_NAME = Qt::red;
  12. static const QColor DEFAULT_ATTRIBUTE_VALUE = Qt::darkGreen;
  13. static const QColor DEFAULT_ERROR = Qt::darkMagenta;
  14. static const QColor DEFAULT_OTHER = Qt::black;
  15. // Regular expressions for parsing XML borrowed from:
  16. // http://www.cs.sfu.ca/~cameron/REX.html
  17. static const QString EXPR_COMMENT = "<!--[^-]*-([^-][^-]*-)*->";
  18. static const QString EXPR_COMMENT_BEGIN = "<!--";
  19. static const QString EXPR_COMMENT_END = "[^-]*-([^-][^-]*-)*->";
  20. static const QString EXPR_ATTRIBUTE_VALUE = "\"[^<\"]*\"|'[^<']*'";
  21. static const QString EXPR_NAME = "([A-Za-z_:]|[^\\x00-\\x7F])([A-Za-z0-9_:.-]|[^\\x00-\\x7F])*";
  22. FbHighlighter::FbHighlighter(QObject* parent)
  23. : QSyntaxHighlighter(parent)
  24. {
  25. init();
  26. }
  27. FbHighlighter::FbHighlighter(QTextDocument* parent)
  28. : QSyntaxHighlighter(parent)
  29. {
  30. init();
  31. }
  32. FbHighlighter::FbHighlighter(QTextEdit* parent)
  33. : QSyntaxHighlighter(parent)
  34. {
  35. init();
  36. }
  37. FbHighlighter::~FbHighlighter()
  38. {
  39. }
  40. void FbHighlighter::init()
  41. {
  42. fmtSyntaxChar.setForeground(DEFAULT_SYNTAX_CHAR);
  43. fmtElementName.setForeground(DEFAULT_ELEMENT_NAME);
  44. fmtComment.setForeground(DEFAULT_COMMENT);
  45. fmtAttributeName.setForeground(DEFAULT_ATTRIBUTE_NAME);
  46. fmtAttributeValue.setForeground(DEFAULT_ATTRIBUTE_VALUE);
  47. fmtError.setForeground(DEFAULT_ERROR);
  48. fmtOther.setForeground(DEFAULT_OTHER);
  49. }
  50. void FbHighlighter::setHighlightColor(HighlightType type, QColor color, bool foreground)
  51. {
  52. QTextCharFormat format;
  53. if (foreground)
  54. format.setForeground(color);
  55. else
  56. format.setBackground(color);
  57. setHighlightFormat(type, format);
  58. }
  59. void FbHighlighter::setHighlightFormat(HighlightType type, QTextCharFormat format)
  60. {
  61. switch (type)
  62. {
  63. case SyntaxChar:
  64. fmtSyntaxChar = format;
  65. break;
  66. case ElementName:
  67. fmtElementName = format;
  68. break;
  69. case Comment:
  70. fmtComment = format;
  71. break;
  72. case AttributeName:
  73. fmtAttributeName = format;
  74. break;
  75. case AttributeValue:
  76. fmtAttributeValue = format;
  77. break;
  78. case Error:
  79. fmtError = format;
  80. break;
  81. case Other:
  82. fmtOther = format;
  83. break;
  84. }
  85. rehighlight();
  86. }
  87. void FbHighlighter::highlightBlock(const QString& text)
  88. {
  89. int i = 0;
  90. int pos = 0;
  91. int brackets = 0;
  92. state = (previousBlockState() == InElement ? ExpectAttributeOrEndOfElement : NoState);
  93. if (previousBlockState() == InComment)
  94. {
  95. // search for the end of the comment
  96. QRegExp expression(EXPR_COMMENT_END);
  97. pos = expression.indexIn(text, i);
  98. if (pos >= 0)
  99. {
  100. // end comment found
  101. const int iLength = expression.matchedLength();
  102. setFormat(0, iLength - 3, fmtComment);
  103. setFormat(iLength - 3, 3, fmtSyntaxChar);
  104. i += iLength; // skip comment
  105. }
  106. else
  107. {
  108. // in comment
  109. setFormat(0, text.length(), fmtComment);
  110. setCurrentBlockState(InComment);
  111. return;
  112. }
  113. }
  114. for (; i < text.length(); i++)
  115. {
  116. switch (text.at(i).toAscii())
  117. {
  118. case '<':
  119. brackets++;
  120. if (brackets == 1)
  121. {
  122. setFormat(i, 1, fmtSyntaxChar);
  123. state = ExpectElementNameOrSlash;
  124. }
  125. else
  126. {
  127. // wrong bracket nesting
  128. setFormat(i, 1, fmtError);
  129. }
  130. break;
  131. case '>':
  132. brackets--;
  133. if (brackets == 0)
  134. {
  135. setFormat(i, 1, fmtSyntaxChar);
  136. }
  137. else
  138. {
  139. // wrong bracket nesting
  140. setFormat( i, 1, fmtError);
  141. }
  142. state = NoState;
  143. break;
  144. case '/':
  145. if (state == ExpectElementNameOrSlash)
  146. {
  147. state = ExpectElementName;
  148. setFormat(i, 1, fmtSyntaxChar);
  149. }
  150. else
  151. {
  152. if (state == ExpectAttributeOrEndOfElement)
  153. {
  154. setFormat(i, 1, fmtSyntaxChar);
  155. }
  156. else
  157. {
  158. processDefaultText(i, text);
  159. }
  160. }
  161. break;
  162. case '=':
  163. if (state == ExpectEqual)
  164. {
  165. state = ExpectAttributeValue;
  166. setFormat(i, 1, fmtOther);
  167. }
  168. else
  169. {
  170. processDefaultText(i, text);
  171. }
  172. break;
  173. case '\'':
  174. case '\"':
  175. if (state == ExpectAttributeValue)
  176. {
  177. // search attribute value
  178. QRegExp expression(EXPR_ATTRIBUTE_VALUE);
  179. pos = expression.indexIn(text, i);
  180. if (pos == i) // attribute value found ?
  181. {
  182. const int iLength = expression.matchedLength();
  183. setFormat(i, 1, fmtOther);
  184. setFormat(i + 1, iLength - 2, fmtAttributeValue);
  185. setFormat(i + iLength - 1, 1, fmtOther);
  186. i += iLength - 1; // skip attribute value
  187. state = ExpectAttributeOrEndOfElement;
  188. }
  189. else
  190. {
  191. processDefaultText(i, text);
  192. }
  193. }
  194. else
  195. {
  196. processDefaultText(i, text);
  197. }
  198. break;
  199. case '!':
  200. if (state == ExpectElementNameOrSlash)
  201. {
  202. // search comment
  203. QRegExp expression(EXPR_COMMENT);
  204. pos = expression.indexIn(text, i - 1);
  205. if (pos == i - 1) // comment found ?
  206. {
  207. const int iLength = expression.matchedLength();
  208. setFormat(pos, 4, fmtSyntaxChar);
  209. setFormat(pos + 4, iLength - 7, fmtComment);
  210. setFormat(iLength - 3, 3, fmtSyntaxChar);
  211. i += iLength - 2; // skip comment
  212. state = NoState;
  213. brackets--;
  214. }
  215. else
  216. {
  217. // Try find multiline comment
  218. QRegExp expression(EXPR_COMMENT_BEGIN); // search comment start
  219. pos = expression.indexIn(text, i - 1);
  220. //if (pos == i - 1) // comment found ?
  221. if (pos >= i - 1)
  222. {
  223. setFormat(i, 3, fmtSyntaxChar);
  224. setFormat(i + 3, text.length() - i - 3, fmtComment);
  225. setCurrentBlockState(InComment);
  226. return;
  227. }
  228. else
  229. {
  230. processDefaultText(i, text);
  231. }
  232. }
  233. }
  234. else
  235. {
  236. processDefaultText(i, text);
  237. }
  238. break;
  239. default:
  240. const int iLength = processDefaultText(i, text);
  241. if (iLength > 0)
  242. i += iLength - 1;
  243. break;
  244. }
  245. }
  246. if (state == ExpectAttributeOrEndOfElement)
  247. {
  248. setCurrentBlockState(InElement);
  249. }
  250. }
  251. int FbHighlighter::processDefaultText(int i, const QString& text)
  252. {
  253. // length of matched text
  254. int iLength = 0;
  255. switch(state)
  256. {
  257. case ExpectElementNameOrSlash:
  258. case ExpectElementName:
  259. {
  260. // search element name
  261. QRegExp expression(EXPR_NAME);
  262. const int pos = expression.indexIn(text, i);
  263. if (pos == i) // found ?
  264. {
  265. iLength = expression.matchedLength();
  266. setFormat(pos, iLength, fmtElementName);
  267. state = ExpectAttributeOrEndOfElement;
  268. }
  269. else
  270. {
  271. setFormat(i, 1, fmtOther);
  272. }
  273. }
  274. break;
  275. case ExpectAttributeOrEndOfElement:
  276. {
  277. // search attribute name
  278. QRegExp expression(EXPR_NAME);
  279. const int pos = expression.indexIn(text, i);
  280. if (pos == i) // found ?
  281. {
  282. iLength = expression.matchedLength();
  283. setFormat(pos, iLength, fmtAttributeName);
  284. state = ExpectEqual;
  285. }
  286. else
  287. {
  288. setFormat(i, 1, fmtOther);
  289. }
  290. }
  291. break;
  292. default:
  293. setFormat(i, 1, fmtOther);
  294. break;
  295. }
  296. return iLength;
  297. }
  298. //---------------------------------------------------------------------------
  299. // FbCodeEdit
  300. //---------------------------------------------------------------------------
  301. qreal FbCodeEdit::baseFontSize = 10;
  302. qreal FbCodeEdit::zoomRatioMin = 0.2;
  303. qreal FbCodeEdit::zoomRatioMax = 5.0;
  304. FbCodeEdit::FbCodeEdit(QWidget *parent) : QPlainTextEdit(parent)
  305. {
  306. lineNumberArea = new LineNumberArea(this);
  307. highlighter = new FbHighlighter(this);
  308. highlighter->setDocument( document() );
  309. connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
  310. connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
  311. connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));
  312. zoomRatio = 1;
  313. #ifdef Q_WS_WIN
  314. setFont(QFont("Courier New", baseFontSize));
  315. #else
  316. setFont(QFont("Monospace", baseFontSize));
  317. #endif
  318. updateLineNumberAreaWidth(0);
  319. highlightCurrentLine();
  320. }
  321. QAction * FbCodeEdit::act(Fb::Actions index) const
  322. {
  323. return m_actions[index];
  324. }
  325. void FbCodeEdit::setAction(Fb::Actions index, QAction *action)
  326. {
  327. m_actions[index] = action;
  328. }
  329. void FbCodeEdit::connectActions(QToolBar *tool)
  330. {
  331. act(Fb::EditUndo)->setEnabled(document()->isUndoAvailable());
  332. act(Fb::EditRedo)->setEnabled(document()->isRedoAvailable());
  333. act(Fb::EditFind)->setEnabled(true);
  334. act(Fb::ZoomIn)->setEnabled(true);
  335. act(Fb::ZoomOut)->setEnabled(true);
  336. act(Fb::ZoomReset)->setEnabled(true);
  337. connect(act(Fb::EditUndo), SIGNAL(triggered()), SLOT(undo()));
  338. connect(act(Fb::EditRedo), SIGNAL(triggered()), SLOT(redo()));
  339. connect(this, SIGNAL(undoAvailable(bool)), act(Fb::EditUndo), SLOT(setEnabled(bool)));
  340. connect(this, SIGNAL(redoAvailable(bool)), act(Fb::EditRedo), SLOT(setEnabled(bool)));
  341. connect(act(Fb::EditCut), SIGNAL(triggered()), SLOT(cut()));
  342. connect(act(Fb::EditCopy), SIGNAL(triggered()), SLOT(copy()));
  343. connect(act(Fb::EditPaste), SIGNAL(triggered()), SLOT(paste()));
  344. connect(act(Fb::EditFind), SIGNAL(triggered()), SLOT(find()));
  345. connect(this, SIGNAL(copyAvailable(bool)), act(Fb::EditCut), SLOT(setEnabled(bool)));
  346. connect(this, SIGNAL(copyAvailable(bool)), act(Fb::EditCopy), SLOT(setEnabled(bool)));
  347. connect(qApp->clipboard(), SIGNAL(dataChanged()), SLOT(clipboardDataChanged()));
  348. clipboardDataChanged();
  349. connect(act(Fb::ZoomIn), SIGNAL(triggered()), SLOT(zoomIn()));
  350. connect(act(Fb::ZoomOut), SIGNAL(triggered()), SLOT(zoomOut()));
  351. connect(act(Fb::ZoomReset), SIGNAL(triggered()), SLOT(zoomReset()));
  352. tool->clear();
  353. tool->addSeparator();
  354. tool->addAction(act(Fb::EditUndo));
  355. tool->addAction(act(Fb::EditRedo));
  356. tool->addSeparator();
  357. tool->addAction(act(Fb::EditCut));
  358. tool->addAction(act(Fb::EditCopy));
  359. tool->addAction(act(Fb::EditPaste));
  360. tool->addSeparator();
  361. tool->addAction(act(Fb::ZoomIn));
  362. tool->addAction(act(Fb::ZoomOut));
  363. tool->addAction(act(Fb::ZoomReset));
  364. }
  365. void FbCodeEdit::disconnectActions()
  366. {
  367. m_actions.disconnect();
  368. }
  369. void FbCodeEdit::clipboardDataChanged()
  370. {
  371. if (const QMimeData *md = QApplication::clipboard()->mimeData()) {
  372. act(Fb::EditPaste)->setEnabled(md->hasText());
  373. }
  374. }
  375. bool FbCodeEdit::read(QIODevice *device)
  376. {
  377. QByteArray data = device->readAll();
  378. QXmlInputSource source;
  379. source.setData(data);
  380. setPlainText(source.data());
  381. return true;
  382. }
  383. int FbCodeEdit::lineNumberAreaWidth()
  384. {
  385. int digits = 1;
  386. int max = qMax(1, blockCount());
  387. while (max >= 10) {
  388. max /= 10;
  389. ++digits;
  390. }
  391. int space = 3 + fontMetrics().width(QLatin1Char('9')) * digits;
  392. return space;
  393. }
  394. void FbCodeEdit::updateLineNumberAreaWidth(int /* newBlockCount */)
  395. {
  396. setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
  397. }
  398. void FbCodeEdit::updateLineNumberArea(const QRect &rect, int dy)
  399. {
  400. if (dy)
  401. lineNumberArea->scroll(0, dy);
  402. else
  403. lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
  404. if (rect.contains(viewport()->rect()))
  405. updateLineNumberAreaWidth(0);
  406. }
  407. void FbCodeEdit::resizeEvent(QResizeEvent *e)
  408. {
  409. QPlainTextEdit::resizeEvent(e);
  410. QRect cr = contentsRect();
  411. lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
  412. }
  413. void FbCodeEdit::highlightCurrentLine()
  414. {
  415. QList<QTextEdit::ExtraSelection> extraSelections;
  416. if (!isReadOnly()) {
  417. QTextEdit::ExtraSelection selection;
  418. QColor lineColor = QColor(Qt::yellow).lighter(160);
  419. selection.format.setBackground(lineColor);
  420. selection.format.setProperty(QTextFormat::FullWidthSelection, true);
  421. selection.cursor = textCursor();
  422. selection.cursor.clearSelection();
  423. extraSelections.append(selection);
  424. }
  425. setExtraSelections(extraSelections);
  426. }
  427. void FbCodeEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
  428. {
  429. QPainter painter(lineNumberArea);
  430. painter.fillRect(event->rect(), Qt::lightGray);
  431. QTextBlock block = firstVisibleBlock();
  432. int blockNumber = block.blockNumber();
  433. int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
  434. int bottom = top + (int) blockBoundingRect(block).height();
  435. while (block.isValid() && top <= event->rect().bottom()) {
  436. if (block.isVisible() && bottom >= event->rect().top()) {
  437. QString number = QString::number(blockNumber + 1);
  438. painter.setPen(Qt::black);
  439. painter.drawText(0, top, lineNumberArea->width(), fontMetrics().height(),
  440. Qt::AlignRight, number);
  441. }
  442. block = block.next();
  443. top = bottom;
  444. bottom = top + (int) blockBoundingRect(block).height();
  445. ++blockNumber;
  446. }
  447. }
  448. bool FbCodeEdit::findText(const QString &exp, QTextDocument::FindFlags options)
  449. {
  450. return QPlainTextEdit::find(exp, options);
  451. }
  452. void FbCodeEdit::find()
  453. {
  454. FbCodeFindDlg dlg(*this);
  455. dlg.exec();
  456. }
  457. void FbCodeEdit::zoomIn()
  458. {
  459. qreal ratio = zoomRatio * 1.1;
  460. ratio = qMin(ratio, zoomRatioMax);
  461. setZoomRatio(ratio);
  462. }
  463. void FbCodeEdit::zoomOut()
  464. {
  465. qreal ratio = zoomRatio / 1.1;
  466. ratio = qMax(ratio, zoomRatioMin);
  467. setZoomRatio(ratio);
  468. }
  469. void FbCodeEdit::zoomReset()
  470. {
  471. setZoomRatio(1.0);
  472. }
  473. void FbCodeEdit::setZoomRatio(qreal ratio)
  474. {
  475. if (!qFuzzyCompare(1 + zoomRatio, 1 + ratio)) {
  476. zoomRatio = ratio;
  477. QFont f = font();
  478. f.setPointSizeF(baseFontSize * zoomRatio);
  479. setFont(f);
  480. }
  481. }