Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525#include "scriptedit.hpp"
#include <vector>
#include <QApplication>#include <QDragEnterEvent>#include <QMenu>#include <QPainter>#include <QString>#include <QTextDocumentFragment>
#include <apps/opencs/model/prefs/category.hpp>#include <apps/opencs/model/prefs/setting.hpp>#include <apps/opencs/model/world/data.hpp>#include <apps/opencs/view/world/scripthighlighter.hpp>
#include "../../model/doc/document.hpp"
#include "../../model/prefs/shortcut.hpp"#include "../../model/prefs/state.hpp"#include "../../model/world/tablemimedata.hpp"#include "../../model/world/universalid.hpp"
namespace{ void prependToEachLine(QTextCursor begin, const QString& text) { QTextCursor end = begin; begin.setPosition(begin.selectionStart()); begin.movePosition(QTextCursor::StartOfLine); end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.insertText(text); } begin.endEditBlock(); }}
CSVWorld::ScriptEdit::ChangeLock::ChangeLock(ScriptEdit& edit) : mEdit(edit){ ++mEdit.mChangeLocked;}
CSVWorld::ScriptEdit::ChangeLock::~ChangeLock(){ --mEdit.mChangeLocked;}
bool CSVWorld::ScriptEdit::event(QEvent* event){ // ignore undo and redo shortcuts if (event->type() == QEvent::ShortcutOverride) { QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->matches(QKeySequence::Undo) || keyEvent->matches(QKeySequence::Redo)) return true; }
return QPlainTextEdit::event(event);}
void CSVWorld::ScriptEdit::keyPressEvent(QKeyEvent* event){ if (event->key() == Qt::Key_Backtab) { QTextCursor cursor = textCursor(); QTextCursor end = cursor; cursor.setPosition(cursor.selectionStart()); cursor.movePosition(QTextCursor::StartOfLine); end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine); cursor.beginEditBlock(); for (; cursor < end; cursor.movePosition(QTextCursor::EndOfLine), cursor.movePosition(QTextCursor::Right)) { cursor.select(QTextCursor::LineUnderCursor); QString line = cursor.selectedText();
if (line.isEmpty()) continue; qsizetype index = 0; if (line[0] == '\t') index = 1; else { // Remove up to a tab worth of spaces instead while (line[index].isSpace() && index < mTabCharCount && line[index] != '\t') index++; }
if (index != 0) { line.remove(0, index); cursor.insertText(line); } } cursor.endEditBlock(); return; } else if (event->key() == Qt::Key_Tab) { QTextCursor cursor = textCursor(); if (cursor.hasSelection()) { prependToEachLine(cursor, "\t"); return; } } QPlainTextEdit::keyPressEvent(event);}
CSVWorld::ScriptEdit::ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent) : QPlainTextEdit(parent) , mChangeLocked(0) , mShowLineNum(false) , mLineNumberArea(nullptr) , mDefaultFont(font()) , mMonoFont(QFont("Monospace")) , mTabCharCount(4) , mMarkOccurrences(true) , mDocument(document) , mWhiteListQuotes("^[a-zA-Z_][a-zA-Z0-9_]*$"){ wrapLines(false); setTabWidth(); setUndoRedoEnabled(false); // we use OpenCS-wide undo/redo instead
mAllowedTypes << CSMWorld::UniversalId::Type_Journal << CSMWorld::UniversalId::Type_Global << CSMWorld::UniversalId::Type_Topic << CSMWorld::UniversalId::Type_Sound << CSMWorld::UniversalId::Type_Spell << CSMWorld::UniversalId::Type_Cell << CSMWorld::UniversalId::Type_Referenceable << CSMWorld::UniversalId::Type_Activator << CSMWorld::UniversalId::Type_Potion << CSMWorld::UniversalId::Type_Apparatus << CSMWorld::UniversalId::Type_Armor << CSMWorld::UniversalId::Type_Book << CSMWorld::UniversalId::Type_Clothing << CSMWorld::UniversalId::Type_Container << CSMWorld::UniversalId::Type_Creature << CSMWorld::UniversalId::Type_Door << CSMWorld::UniversalId::Type_Ingredient << CSMWorld::UniversalId::Type_CreatureLevelledList << CSMWorld::UniversalId::Type_ItemLevelledList << CSMWorld::UniversalId::Type_Light << CSMWorld::UniversalId::Type_Lockpick << CSMWorld::UniversalId::Type_Miscellaneous << CSMWorld::UniversalId::Type_Npc << CSMWorld::UniversalId::Type_Probe << CSMWorld::UniversalId::Type_Repair << CSMWorld::UniversalId::Type_Static << CSMWorld::UniversalId::Type_Weapon << CSMWorld::UniversalId::Type_Script << CSMWorld::UniversalId::Type_Region;
connect(this, &ScriptEdit::cursorPositionChanged, this, &ScriptEdit::markOccurrences);
mCommentAction = new QAction(tr("Comment Selection"), this); connect(mCommentAction, &QAction::triggered, this, &ScriptEdit::commentSelection); CSMPrefs::Shortcut* commentShortcut = new CSMPrefs::Shortcut("script-editor-comment", this); commentShortcut->associateAction(mCommentAction);
mUncommentAction = new QAction(tr("Uncomment Selection"), this); connect(mUncommentAction, &QAction::triggered, this, &ScriptEdit::uncommentSelection); CSMPrefs::Shortcut* uncommentShortcut = new CSMPrefs::Shortcut("script-editor-uncomment", this); uncommentShortcut->associateAction(mUncommentAction);
mHighlighter = new ScriptHighlighter(document.getData(), mode, ScriptEdit::document());
connect(&document.getData(), &CSMWorld::Data::idListChanged, this, &ScriptEdit::idListChanged);
connect(&mUpdateTimer, &QTimer::timeout, this, &ScriptEdit::updateHighlighting);
connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptEdit::settingChanged); { ChangeLock lock(*this); CSMPrefs::get()["Scripts"].update(); }
mUpdateTimer.setSingleShot(true);
// TODO: provide a font selector dialogue mMonoFont.setStyleHint(QFont::TypeWriter);
mLineNumberArea = new LineNumberArea(this); updateLineNumberAreaWidth(0);
connect(this, &ScriptEdit::blockCountChanged, this, &ScriptEdit::updateLineNumberAreaWidth); connect(this, &ScriptEdit::updateRequest, this, &ScriptEdit::updateLineNumberArea); updateHighlighting();}
void CSVWorld::ScriptEdit::showLineNum(bool show){ if (show != mShowLineNum) { mShowLineNum = show; updateLineNumberAreaWidth(0); }}
bool CSVWorld::ScriptEdit::isChangeLocked() const{ return mChangeLocked != 0;}
void CSVWorld::ScriptEdit::dragEnterEvent(QDragEnterEvent* event){ const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*>(event->mimeData()); if (!mime) QPlainTextEdit::dragEnterEvent(event); else { setTextCursor(cursorForPosition(event->position().toPoint())); event->acceptProposedAction(); }}
void CSVWorld::ScriptEdit::dragMoveEvent(QDragMoveEvent* event){ const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*>(event->mimeData()); if (!mime) QPlainTextEdit::dragMoveEvent(event); else { setTextCursor(cursorForPosition(event->position().toPoint())); event->accept(); }}
void CSVWorld::ScriptEdit::dropEvent(QDropEvent* event){ const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*>(event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped { QPlainTextEdit::dropEvent(event); return; }
setTextCursor(cursorForPosition(event->position().toPoint()));
if (mime->fromDocument(mDocument)) { std::vector<CSMWorld::UniversalId> records(mime->getData());
for (std::vector<CSMWorld::UniversalId>::iterator it = records.begin(); it != records.end(); ++it) { if (mAllowedTypes.contains(it->getType())) { if (stringNeedsQuote(it->getId())) { insertPlainText(QString::fromUtf8(('"' + it->getId() + '"').c_str())); } else { insertPlainText(QString::fromUtf8(it->getId().c_str())); } } } }}
bool CSVWorld::ScriptEdit::stringNeedsQuote(const std::string& id) const{ const QString string(QString::fromUtf8(id.c_str())); // I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other than… return !(string.contains(mWhiteListQuotes));}
void CSVWorld::ScriptEdit::setTabWidth(){ // Set tab width to specified number of characters using current font. setTabStopDistance(mTabCharCount * fontMetrics().horizontalAdvance(' '));}
void CSVWorld::ScriptEdit::wrapLines(bool wrap){ if (wrap) { setLineWrapMode(QPlainTextEdit::WidgetWidth); } else { setLineWrapMode(QPlainTextEdit::NoWrap); }}
void CSVWorld::ScriptEdit::settingChanged(const CSMPrefs::Setting* setting){ // Determine which setting was changed. if (mHighlighter->settingChanged(setting)) { updateHighlighting(); } else if (*setting == "Scripts/mono-font") { setFont(setting->isTrue() ? mMonoFont : mDefaultFont); setTabWidth(); } else if (*setting == "Scripts/show-linenum") { showLineNum(setting->isTrue()); } else if (*setting == "Scripts/wrap-lines") { wrapLines(setting->isTrue()); } else if (*setting == "Scripts/tab-width") { mTabCharCount = setting->toInt(); setTabWidth(); } else if (*setting == "Scripts/highlight-occurrences") { mMarkOccurrences = setting->isTrue(); mHighlighter->setMarkedWord(""); updateHighlighting(); mHighlighter->setMarkOccurrences(mMarkOccurrences); }}
void CSVWorld::ScriptEdit::idListChanged(){ mHighlighter->invalidateIds();
if (!mUpdateTimer.isActive()) mUpdateTimer.start(0);}
void CSVWorld::ScriptEdit::updateHighlighting(){ if (isChangeLocked()) return;
ChangeLock lock(*this);
mHighlighter->rehighlight();}
int CSVWorld::ScriptEdit::lineNumberAreaWidth(){ if (!mShowLineNum) return 0;
int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; }
int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; return space;}
void CSVWorld::ScriptEdit::updateLineNumberAreaWidth(int /* newBlockCount */){ setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);}
void CSVWorld::ScriptEdit::updateLineNumberArea(const QRect& rect, int dy){ if (dy) mLineNumberArea->scroll(0, dy); else mLineNumberArea->update(0, rect.y(), mLineNumberArea->width(), rect.height());
if (rect.contains(viewport()->rect())) updateLineNumberAreaWidth(0);}
void CSVWorld::ScriptEdit::markOccurrences(){ if (mMarkOccurrences) { QTextCursor cursor = textCursor();
// prevent infinite recursion with cursor.select(), // which ends up calling this function again // could be fixed with blockSignals, but mDocument is const disconnect(this, &ScriptEdit::cursorPositionChanged, this, nullptr); cursor.select(QTextCursor::WordUnderCursor); connect(this, &ScriptEdit::cursorPositionChanged, this, &ScriptEdit::markOccurrences);
QString word = cursor.selectedText(); mHighlighter->setMarkedWord(word.toStdString()); mHighlighter->rehighlight(); }}
void CSVWorld::ScriptEdit::commentSelection(){ prependToEachLine(textCursor(), ";");}
void CSVWorld::ScriptEdit::uncommentSelection(){ QTextCursor begin = textCursor(); QTextCursor end = begin; begin.setPosition(begin.selectionStart()); begin.movePosition(QTextCursor::StartOfLine);
end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine);
begin.beginEditBlock(); for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.select(QTextCursor::LineUnderCursor); QString line = begin.selectedText();
if (line.isEmpty()) continue;
// get first nonspace character in line qsizetype index; for (index = 0; index != line.size(); ++index) { if (!line[index].isSpace()) break; }
if (index != line.size() && line[index] == ';') { // remove the semicolon line.remove(index, 1); // put the line back begin.insertText(line); } }
begin.endEditBlock();}
void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent* e){ QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect(); mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));}
void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent* event){ QMenu* menu = createStandardContextMenu();
// remove redo/undo since they are disabled QList<QAction*> menuActions = menu->actions(); for (QList<QAction*>::iterator i = menuActions.begin(); i < menuActions.end(); ++i) { if ((*i)->text().contains("Undo") || (*i)->text().contains("Redo")) { (*i)->setVisible(false); } } menu->addAction(mCommentAction); menu->addAction(mUncommentAction);
menu->exec(event->globalPos()); delete menu;}
void CSVWorld::ScriptEdit::lineNumberAreaPaintEvent(QPaintEvent* event){ QPainter painter(mLineNumberArea);
QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = (int)blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + (int)blockBoundingRect(block).height();
int startBlock = textCursor().blockNumber(); int endBlock = textCursor().blockNumber(); if (textCursor().hasSelection()) { QString str = textCursor().selection().toPlainText(); int offset = str.count("\n"); if (textCursor().position() < textCursor().anchor()) endBlock += offset; else startBlock -= offset; } painter.setBackgroundMode(Qt::OpaqueMode); QFont font = painter.font(); QBrush background = painter.background(); QColor textColor = QApplication::palette().text().color();
while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { QFont newFont = painter.font(); QString number = QString::number(blockNumber + 1); if (blockNumber >= startBlock && blockNumber <= endBlock) { painter.setBackground(Qt::cyan); painter.setPen(Qt::darkMagenta); newFont.setBold(true); } else { painter.setBackground(background); painter.setPen(textColor); } painter.setFont(newFont); painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); painter.setFont(font); }
block = block.next(); top = bottom; bottom = top + (int)blockBoundingRect(block).height(); ++blockNumber; }}
CSVWorld::LineNumberArea::LineNumberArea(ScriptEdit* editor) : QWidget(editor) , mScriptEdit(editor){}
QSize CSVWorld::LineNumberArea::sizeHint() const{ return QSize(mScriptEdit->lineNumberAreaWidth(), 0);}
void CSVWorld::LineNumberArea::paintEvent(QPaintEvent* event){ mScriptEdit->lineNumberAreaPaintEvent(event);}