1 #include "scriptedit.hpp"
2
3 #include <algorithm>
4
5 #include <QDragEnterEvent>
6 #include <QRegExp>
7 #include <QString>
8 #include <QPainter>
9 #include <QTextDocumentFragment>
10 #include <QMenu>
11
12 #include "../../model/doc/document.hpp"
13
14 #include "../../model/world/universalid.hpp"
15 #include "../../model/world/tablemimedata.hpp"
16 #include "../../model/prefs/state.hpp"
17 #include "../../model/prefs/shortcut.hpp"
18
ChangeLock(ScriptEdit & edit)19 CSVWorld::ScriptEdit::ChangeLock::ChangeLock (ScriptEdit& edit) : mEdit (edit)
20 {
21 ++mEdit.mChangeLocked;
22 }
23
~ChangeLock()24 CSVWorld::ScriptEdit::ChangeLock::~ChangeLock()
25 {
26 --mEdit.mChangeLocked;
27 }
28
event(QEvent * event)29 bool CSVWorld::ScriptEdit::event (QEvent *event)
30 {
31 // ignore undo and redo shortcuts
32 if (event->type()==QEvent::ShortcutOverride)
33 {
34 QKeyEvent *keyEvent = static_cast<QKeyEvent *> (event);
35
36 if (keyEvent->matches (QKeySequence::Undo) || keyEvent->matches (QKeySequence::Redo))
37 return true;
38 }
39
40 return QPlainTextEdit::event (event);
41 }
42
ScriptEdit(const CSMDoc::Document & document,ScriptHighlighter::Mode mode,QWidget * parent)43 CSVWorld::ScriptEdit::ScriptEdit(
44 const CSMDoc::Document& document,
45 ScriptHighlighter::Mode mode,
46 QWidget* parent
47 ) : QPlainTextEdit(parent),
48 mChangeLocked(0),
49 mShowLineNum(false),
50 mLineNumberArea(nullptr),
51 mDefaultFont(font()),
52 mMonoFont(QFont("Monospace")),
53 mTabCharCount(4),
54 mMarkOccurrences(true),
55 mDocument(document),
56 mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive)
57 {
58 wrapLines(false);
59 setTabWidth();
60 setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead
61
62 mAllowedTypes <<CSMWorld::UniversalId::Type_Journal
63 <<CSMWorld::UniversalId::Type_Global
64 <<CSMWorld::UniversalId::Type_Topic
65 <<CSMWorld::UniversalId::Type_Sound
66 <<CSMWorld::UniversalId::Type_Spell
67 <<CSMWorld::UniversalId::Type_Cell
68 <<CSMWorld::UniversalId::Type_Referenceable
69 <<CSMWorld::UniversalId::Type_Activator
70 <<CSMWorld::UniversalId::Type_Potion
71 <<CSMWorld::UniversalId::Type_Apparatus
72 <<CSMWorld::UniversalId::Type_Armor
73 <<CSMWorld::UniversalId::Type_Book
74 <<CSMWorld::UniversalId::Type_Clothing
75 <<CSMWorld::UniversalId::Type_Container
76 <<CSMWorld::UniversalId::Type_Creature
77 <<CSMWorld::UniversalId::Type_Door
78 <<CSMWorld::UniversalId::Type_Ingredient
79 <<CSMWorld::UniversalId::Type_CreatureLevelledList
80 <<CSMWorld::UniversalId::Type_ItemLevelledList
81 <<CSMWorld::UniversalId::Type_Light
82 <<CSMWorld::UniversalId::Type_Lockpick
83 <<CSMWorld::UniversalId::Type_Miscellaneous
84 <<CSMWorld::UniversalId::Type_Npc
85 <<CSMWorld::UniversalId::Type_Probe
86 <<CSMWorld::UniversalId::Type_Repair
87 <<CSMWorld::UniversalId::Type_Static
88 <<CSMWorld::UniversalId::Type_Weapon
89 <<CSMWorld::UniversalId::Type_Script
90 <<CSMWorld::UniversalId::Type_Region;
91
92 connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(markOccurrences()));
93
94 mCommentAction = new QAction (tr ("Comment Selection"), this);
95 connect(mCommentAction, SIGNAL (triggered()), this, SLOT (commentSelection()));
96 CSMPrefs::Shortcut *commentShortcut = new CSMPrefs::Shortcut("script-editor-comment", this);
97 commentShortcut->associateAction(mCommentAction);
98
99 mUncommentAction = new QAction (tr ("Uncomment Selection"), this);
100 connect(mUncommentAction, SIGNAL (triggered()), this, SLOT (uncommentSelection()));
101 CSMPrefs::Shortcut *uncommentShortcut = new CSMPrefs::Shortcut("script-editor-uncomment", this);
102 uncommentShortcut->associateAction(mUncommentAction);
103
104 mHighlighter = new ScriptHighlighter (document.getData(), mode, ScriptEdit::document());
105
106 connect (&document.getData(), SIGNAL (idListChanged()), this, SLOT (idListChanged()));
107
108 connect (&mUpdateTimer, SIGNAL (timeout()), this, SLOT (updateHighlighting()));
109
110 connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)),
111 this, SLOT (settingChanged (const CSMPrefs::Setting *)));
112 {
113 ChangeLock lock (*this);
114 CSMPrefs::get()["Scripts"].update();
115 }
116
117 mUpdateTimer.setSingleShot (true);
118
119 // TODO: provide a font selector dialogue
120 mMonoFont.setStyleHint(QFont::TypeWriter);
121
122 mLineNumberArea = new LineNumberArea(this);
123 updateLineNumberAreaWidth(0);
124
125 connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
126 connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
127 updateHighlighting();
128 }
129
showLineNum(bool show)130 void CSVWorld::ScriptEdit::showLineNum(bool show)
131 {
132 if(show!=mShowLineNum)
133 {
134 mShowLineNum = show;
135 updateLineNumberAreaWidth(0);
136 }
137 }
138
isChangeLocked() const139 bool CSVWorld::ScriptEdit::isChangeLocked() const
140 {
141 return mChangeLocked!=0;
142 }
143
dragEnterEvent(QDragEnterEvent * event)144 void CSVWorld::ScriptEdit::dragEnterEvent (QDragEnterEvent* event)
145 {
146 const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*> (event->mimeData());
147 if (!mime)
148 QPlainTextEdit::dragEnterEvent(event);
149 else
150 {
151 setTextCursor (cursorForPosition (event->pos()));
152 event->acceptProposedAction();
153 }
154 }
155
dragMoveEvent(QDragMoveEvent * event)156 void CSVWorld::ScriptEdit::dragMoveEvent (QDragMoveEvent* event)
157 {
158 const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*> (event->mimeData());
159 if (!mime)
160 QPlainTextEdit::dragMoveEvent(event);
161 else
162 {
163 setTextCursor (cursorForPosition (event->pos()));
164 event->accept();
165 }
166 }
167
dropEvent(QDropEvent * event)168 void CSVWorld::ScriptEdit::dropEvent (QDropEvent* event)
169 {
170 const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*> (event->mimeData());
171 if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped
172 {
173 QPlainTextEdit::dropEvent(event);
174 return;
175 }
176
177 setTextCursor (cursorForPosition (event->pos()));
178
179 if (mime->fromDocument (mDocument))
180 {
181 std::vector<CSMWorld::UniversalId> records (mime->getData());
182
183 for (std::vector<CSMWorld::UniversalId>::iterator it = records.begin(); it != records.end(); ++it)
184 {
185 if (mAllowedTypes.contains (it->getType()))
186 {
187 if (stringNeedsQuote(it->getId()))
188 {
189 insertPlainText(QString::fromUtf8 (('"' + it->getId() + '"').c_str()));
190 } else {
191 insertPlainText(QString::fromUtf8 (it->getId().c_str()));
192 }
193 }
194 }
195 }
196 }
197
stringNeedsQuote(const std::string & id) const198 bool CSVWorld::ScriptEdit::stringNeedsQuote (const std::string& id) const
199 {
200 const QString string(QString::fromUtf8(id.c_str())); //<regex> is only for c++11, so let's use qregexp for now.
201 //I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other than…
202 return !(string.contains(mWhiteListQoutes));
203 }
204
setTabWidth()205 void CSVWorld::ScriptEdit::setTabWidth()
206 {
207 // Set tab width to specified number of characters using current font.
208 setTabStopDistance(mTabCharCount * fontMetrics().horizontalAdvance(' '));
209 }
210
wrapLines(bool wrap)211 void CSVWorld::ScriptEdit::wrapLines(bool wrap)
212 {
213 if (wrap)
214 {
215 setLineWrapMode(QPlainTextEdit::WidgetWidth);
216 }
217 else
218 {
219 setLineWrapMode(QPlainTextEdit::NoWrap);
220 }
221 }
222
settingChanged(const CSMPrefs::Setting * setting)223 void CSVWorld::ScriptEdit::settingChanged(const CSMPrefs::Setting *setting)
224 {
225 // Determine which setting was changed.
226 if (mHighlighter->settingChanged(setting))
227 {
228 updateHighlighting();
229 }
230 else if (*setting == "Scripts/mono-font")
231 {
232 setFont(setting->isTrue() ? mMonoFont : mDefaultFont);
233 setTabWidth();
234 }
235 else if (*setting == "Scripts/show-linenum")
236 {
237 showLineNum(setting->isTrue());
238 }
239 else if (*setting == "Scripts/wrap-lines")
240 {
241 wrapLines(setting->isTrue());
242 }
243 else if (*setting == "Scripts/tab-width")
244 {
245 mTabCharCount = setting->toInt();
246 setTabWidth();
247 }
248 else if (*setting == "Scripts/highlight-occurrences")
249 {
250 mMarkOccurrences = setting->isTrue();
251 mHighlighter->setMarkedWord("");
252 updateHighlighting();
253 mHighlighter->setMarkOccurrences(mMarkOccurrences);
254 }
255 }
256
idListChanged()257 void CSVWorld::ScriptEdit::idListChanged()
258 {
259 mHighlighter->invalidateIds();
260
261 if (!mUpdateTimer.isActive())
262 mUpdateTimer.start (0);
263 }
264
updateHighlighting()265 void CSVWorld::ScriptEdit::updateHighlighting()
266 {
267 if (isChangeLocked())
268 return;
269
270 ChangeLock lock (*this);
271
272 mHighlighter->rehighlight();
273 }
274
lineNumberAreaWidth()275 int CSVWorld::ScriptEdit::lineNumberAreaWidth()
276 {
277 if(!mShowLineNum)
278 return 0;
279
280 int digits = 1;
281 int max = qMax(1, blockCount());
282 while (max >= 10)
283 {
284 max /= 10;
285 ++digits;
286 }
287
288 int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
289 return space;
290 }
291
updateLineNumberAreaWidth(int)292 void CSVWorld::ScriptEdit::updateLineNumberAreaWidth(int /* newBlockCount */)
293 {
294 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
295 }
296
updateLineNumberArea(const QRect & rect,int dy)297 void CSVWorld::ScriptEdit::updateLineNumberArea(const QRect &rect, int dy)
298 {
299 if (dy)
300 mLineNumberArea->scroll(0, dy);
301 else
302 mLineNumberArea->update(0, rect.y(), mLineNumberArea->width(), rect.height());
303
304 if (rect.contains(viewport()->rect()))
305 updateLineNumberAreaWidth(0);
306 }
307
markOccurrences()308 void CSVWorld::ScriptEdit::markOccurrences()
309 {
310 if (mMarkOccurrences)
311 {
312 QTextCursor cursor = textCursor();
313
314 // prevent infinite recursion with cursor.select(),
315 // which ends up calling this function again
316 // could be fixed with blockSignals, but mDocument is const
317 disconnect(this, SIGNAL(cursorPositionChanged()), this, nullptr);
318 cursor.select(QTextCursor::WordUnderCursor);
319 connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(markOccurrences()));
320
321 QString word = cursor.selectedText();
322 mHighlighter->setMarkedWord(word.toStdString());
323 mHighlighter->rehighlight();
324 }
325 }
326
commentSelection()327 void CSVWorld::ScriptEdit::commentSelection()
328 {
329 QTextCursor begin = textCursor();
330 QTextCursor end = begin;
331 begin.setPosition(begin.selectionStart());
332 begin.movePosition(QTextCursor::StartOfLine);
333
334 end.setPosition(end.selectionEnd());
335 end.movePosition(QTextCursor::EndOfLine);
336
337 begin.beginEditBlock();
338
339 for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right))
340 {
341 begin.insertText(";");
342 }
343
344 begin.endEditBlock();
345 }
346
uncommentSelection()347 void CSVWorld::ScriptEdit::uncommentSelection()
348 {
349 QTextCursor begin = textCursor();
350 QTextCursor end = begin;
351 begin.setPosition(begin.selectionStart());
352 begin.movePosition(QTextCursor::StartOfLine);
353
354 end.setPosition(end.selectionEnd());
355 end.movePosition(QTextCursor::EndOfLine);
356
357 begin.beginEditBlock();
358
359 for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) {
360 begin.select(QTextCursor::LineUnderCursor);
361 QString line = begin.selectedText();
362
363 if (line.size() == 0)
364 continue;
365
366 // get first nonspace character in line
367 int index;
368 for (index = 0; index != line.size(); ++index)
369 {
370 if (!line[index].isSpace())
371 break;
372 }
373
374 if (index != line.size() && line[index] == ';')
375 {
376 // remove the semicolon
377 line.remove(index, 1);
378 // put the line back
379 begin.insertText(line);
380 }
381 }
382
383 begin.endEditBlock();
384 }
385
resizeEvent(QResizeEvent * e)386 void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent *e)
387 {
388 QPlainTextEdit::resizeEvent(e);
389
390 QRect cr = contentsRect();
391 mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
392 }
393
contextMenuEvent(QContextMenuEvent * event)394 void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent *event)
395 {
396 QMenu *menu = createStandardContextMenu();
397
398 // remove redo/undo since they are disabled
399 QList<QAction*> menuActions = menu->actions();
400 for (QList<QAction*>::iterator i = menuActions.begin(); i < menuActions.end(); ++i)
401 {
402 if ((*i)->text().contains("Undo") || (*i)->text().contains("Redo"))
403 {
404 (*i)->setVisible(false);
405 }
406 }
407 menu->addAction(mCommentAction);
408 menu->addAction(mUncommentAction);
409
410 menu->exec(event->globalPos());
411 delete menu;
412 }
413
lineNumberAreaPaintEvent(QPaintEvent * event)414 void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
415 {
416 QPainter painter(mLineNumberArea);
417
418 QTextBlock block = firstVisibleBlock();
419 int blockNumber = block.blockNumber();
420 int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
421 int bottom = top + (int) blockBoundingRect(block).height();
422
423 int startBlock = textCursor().blockNumber();
424 int endBlock = textCursor().blockNumber();
425 if(textCursor().hasSelection())
426 {
427 QString str = textCursor().selection().toPlainText();
428 int offset = str.count("\n");
429 if(textCursor().position() < textCursor().anchor())
430 endBlock += offset;
431 else
432 startBlock -= offset;
433 }
434 painter.setBackgroundMode(Qt::OpaqueMode);
435 QFont font = painter.font();
436 QBrush background = painter.background();
437
438 while (block.isValid() && top <= event->rect().bottom())
439 {
440 if (block.isVisible() && bottom >= event->rect().top())
441 {
442 QFont newFont = painter.font();
443 QString number = QString::number(blockNumber + 1);
444 if(blockNumber >= startBlock && blockNumber <= endBlock)
445 {
446 painter.setBackground(Qt::cyan);
447 painter.setPen(Qt::darkMagenta);
448 newFont.setBold(true);
449 }
450 else
451 {
452 painter.setBackground(background);
453 painter.setPen(Qt::black);
454 }
455 painter.setFont(newFont);
456 painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(),
457 Qt::AlignRight, number);
458 painter.setFont(font);
459 }
460
461 block = block.next();
462 top = bottom;
463 bottom = top + (int) blockBoundingRect(block).height();
464 ++blockNumber;
465 }
466 }
467
LineNumberArea(ScriptEdit * editor)468 CSVWorld::LineNumberArea::LineNumberArea(ScriptEdit *editor) : QWidget(editor), mScriptEdit(editor)
469 {}
470
sizeHint() const471 QSize CSVWorld::LineNumberArea::sizeHint() const
472 {
473 return QSize(mScriptEdit->lineNumberAreaWidth(), 0);
474 }
475
paintEvent(QPaintEvent * event)476 void CSVWorld::LineNumberArea::paintEvent(QPaintEvent *event)
477 {
478 mScriptEdit->lineNumberAreaPaintEvent(event);
479 }
480