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